fields.tsx 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067
  1. import {LightWeightOrganization, SelectValue} 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. export type ParsedFunction = {
  23. name: string;
  24. arguments: string[];
  25. };
  26. type ValidateColumnValueFunction = ({name: string, dataType: ColumnType}) => boolean;
  27. export type ValidateColumnTypes = ColumnType[] | ValidateColumnValueFunction;
  28. export type AggregateParameter =
  29. | {
  30. kind: 'column';
  31. columnTypes: Readonly<ValidateColumnTypes>;
  32. defaultValue?: string;
  33. required: boolean;
  34. }
  35. | {
  36. kind: 'value';
  37. dataType: ColumnType;
  38. defaultValue?: string;
  39. required: boolean;
  40. placeholder?: string;
  41. }
  42. | {
  43. kind: 'dropdown';
  44. options: SelectValue<string>[];
  45. dataType: string;
  46. defaultValue?: string;
  47. required: boolean;
  48. placeholder?: string;
  49. };
  50. export type AggregationRefinement = string | undefined;
  51. // The parsed result of a Field.
  52. // Functions and Fields are handled as subtypes to enable other
  53. // code to work more simply.
  54. // This type can be converted into a Field.field using generateFieldAsString()
  55. export type QueryFieldValue =
  56. | {
  57. kind: 'field';
  58. field: string;
  59. }
  60. | {
  61. kind: 'equation';
  62. field: string;
  63. }
  64. | {
  65. kind: 'function';
  66. function: [AggregationKey, string, AggregationRefinement, AggregationRefinement];
  67. };
  68. // Column is just an alias of a Query value
  69. export type Column = QueryFieldValue;
  70. export type Alignments = 'left' | 'right';
  71. const CONDITIONS_ARGUMENTS: SelectValue<string>[] = [
  72. {
  73. label: 'equal =',
  74. value: 'equals',
  75. },
  76. {
  77. label: 'not equal !=',
  78. value: 'notEquals',
  79. },
  80. {
  81. label: 'less <',
  82. value: 'less',
  83. },
  84. {
  85. label: 'greater >',
  86. value: 'greater',
  87. },
  88. {
  89. label: 'less or equals <=',
  90. value: 'lessOrEquals',
  91. },
  92. {
  93. label: 'greater or equals >=',
  94. value: 'greaterOrEquals',
  95. },
  96. ];
  97. // Refer to src/sentry/search/events/fields.py
  98. export const AGGREGATIONS = {
  99. count: {
  100. parameters: [],
  101. outputType: 'number',
  102. isSortable: true,
  103. multiPlotType: 'area',
  104. },
  105. count_unique: {
  106. parameters: [
  107. {
  108. kind: 'column',
  109. columnTypes: ['string', 'integer', 'number', 'duration', 'date', 'boolean'],
  110. required: true,
  111. },
  112. ],
  113. outputType: 'number',
  114. isSortable: true,
  115. multiPlotType: 'line',
  116. },
  117. failure_count: {
  118. parameters: [],
  119. outputType: 'number',
  120. isSortable: true,
  121. multiPlotType: 'line',
  122. },
  123. min: {
  124. parameters: [
  125. {
  126. kind: 'column',
  127. columnTypes: validateForNumericAggregate([
  128. 'integer',
  129. 'number',
  130. 'duration',
  131. 'date',
  132. ]),
  133. required: true,
  134. },
  135. ],
  136. outputType: null,
  137. isSortable: true,
  138. multiPlotType: 'line',
  139. },
  140. max: {
  141. parameters: [
  142. {
  143. kind: 'column',
  144. columnTypes: validateForNumericAggregate([
  145. 'integer',
  146. 'number',
  147. 'duration',
  148. 'date',
  149. ]),
  150. required: true,
  151. },
  152. ],
  153. outputType: null,
  154. isSortable: true,
  155. multiPlotType: 'line',
  156. },
  157. avg: {
  158. parameters: [
  159. {
  160. kind: 'column',
  161. columnTypes: validateForNumericAggregate(['duration', 'number']),
  162. defaultValue: 'transaction.duration',
  163. required: true,
  164. },
  165. ],
  166. outputType: null,
  167. isSortable: true,
  168. multiPlotType: 'line',
  169. },
  170. sum: {
  171. parameters: [
  172. {
  173. kind: 'column',
  174. columnTypes: validateForNumericAggregate(['duration', 'number']),
  175. required: true,
  176. },
  177. ],
  178. outputType: null,
  179. isSortable: true,
  180. multiPlotType: 'area',
  181. },
  182. any: {
  183. parameters: [
  184. {
  185. kind: 'column',
  186. columnTypes: ['string', 'integer', 'number', 'duration', 'date', 'boolean'],
  187. required: true,
  188. },
  189. ],
  190. outputType: null,
  191. isSortable: true,
  192. },
  193. last_seen: {
  194. parameters: [],
  195. outputType: 'date',
  196. isSortable: true,
  197. },
  198. // Tracing functions.
  199. p50: {
  200. parameters: [
  201. {
  202. kind: 'column',
  203. columnTypes: validateForNumericAggregate(['duration', 'number']),
  204. defaultValue: 'transaction.duration',
  205. required: false,
  206. },
  207. ],
  208. outputType: null,
  209. isSortable: true,
  210. multiPlotType: 'line',
  211. },
  212. p75: {
  213. parameters: [
  214. {
  215. kind: 'column',
  216. columnTypes: validateForNumericAggregate(['duration', 'number']),
  217. defaultValue: 'transaction.duration',
  218. required: false,
  219. },
  220. ],
  221. outputType: null,
  222. isSortable: true,
  223. multiPlotType: 'line',
  224. },
  225. p95: {
  226. parameters: [
  227. {
  228. kind: 'column',
  229. columnTypes: validateForNumericAggregate(['duration', 'number']),
  230. defaultValue: 'transaction.duration',
  231. required: false,
  232. },
  233. ],
  234. outputType: null,
  235. type: [],
  236. isSortable: true,
  237. multiPlotType: 'line',
  238. },
  239. p99: {
  240. parameters: [
  241. {
  242. kind: 'column',
  243. columnTypes: validateForNumericAggregate(['duration', 'number']),
  244. defaultValue: 'transaction.duration',
  245. required: false,
  246. },
  247. ],
  248. outputType: null,
  249. isSortable: true,
  250. multiPlotType: 'line',
  251. },
  252. p100: {
  253. parameters: [
  254. {
  255. kind: 'column',
  256. columnTypes: validateForNumericAggregate(['duration', 'number']),
  257. defaultValue: 'transaction.duration',
  258. required: false,
  259. },
  260. ],
  261. outputType: null,
  262. isSortable: true,
  263. multiPlotType: 'line',
  264. },
  265. percentile: {
  266. parameters: [
  267. {
  268. kind: 'column',
  269. columnTypes: validateForNumericAggregate(['duration', 'number']),
  270. defaultValue: 'transaction.duration',
  271. required: true,
  272. },
  273. {
  274. kind: 'value',
  275. dataType: 'number',
  276. defaultValue: '0.5',
  277. required: true,
  278. },
  279. ],
  280. outputType: null,
  281. isSortable: true,
  282. multiPlotType: 'line',
  283. },
  284. failure_rate: {
  285. parameters: [],
  286. outputType: 'percentage',
  287. isSortable: true,
  288. multiPlotType: 'line',
  289. },
  290. apdex: {
  291. getFieldOverrides({parameter, organization}: DefaultValueInputs) {
  292. if (organization.features.includes('project-transaction-threshold')) {
  293. return {required: false, placeholder: 'Automatic', defaultValue: ''};
  294. }
  295. return {
  296. defaultValue: organization.apdexThreshold?.toString() ?? parameter.defaultValue,
  297. };
  298. },
  299. parameters: [
  300. {
  301. kind: 'value',
  302. dataType: 'number',
  303. defaultValue: '300',
  304. required: true,
  305. },
  306. ],
  307. outputType: 'number',
  308. isSortable: true,
  309. multiPlotType: 'line',
  310. },
  311. user_misery: {
  312. getFieldOverrides({parameter, organization}: DefaultValueInputs) {
  313. if (organization.features.includes('project-transaction-threshold')) {
  314. return {required: false, placeholder: 'Automatic', defaultValue: ''};
  315. }
  316. return {
  317. defaultValue: organization.apdexThreshold?.toString() ?? parameter.defaultValue,
  318. };
  319. },
  320. parameters: [
  321. {
  322. kind: 'value',
  323. dataType: 'number',
  324. defaultValue: '300',
  325. required: true,
  326. },
  327. ],
  328. outputType: 'number',
  329. isSortable: true,
  330. multiPlotType: 'area',
  331. },
  332. eps: {
  333. parameters: [],
  334. outputType: 'number',
  335. isSortable: true,
  336. multiPlotType: 'area',
  337. },
  338. epm: {
  339. parameters: [],
  340. outputType: 'number',
  341. isSortable: true,
  342. multiPlotType: 'area',
  343. },
  344. count_miserable: {
  345. getFieldOverrides({parameter, organization}: DefaultValueInputs) {
  346. if (parameter.kind === 'column') {
  347. return {defaultValue: 'user'};
  348. }
  349. if (organization.features.includes('project-transaction-threshold')) {
  350. return {required: false, placeholder: 'Automatic', defaultValue: ''};
  351. }
  352. return {
  353. defaultValue: organization.apdexThreshold?.toString() ?? parameter.defaultValue,
  354. };
  355. },
  356. parameters: [
  357. {
  358. kind: 'column',
  359. columnTypes: validateAllowedColumns(['user']),
  360. defaultValue: 'user',
  361. required: true,
  362. },
  363. {
  364. kind: 'value',
  365. dataType: 'number',
  366. defaultValue: '300',
  367. required: true,
  368. },
  369. ],
  370. outputType: 'number',
  371. isSortable: true,
  372. multiPlotType: 'area',
  373. },
  374. count_if: {
  375. parameters: [
  376. {
  377. kind: 'column',
  378. columnTypes: ['string', 'duration'],
  379. defaultValue: 'transaction.duration',
  380. required: true,
  381. },
  382. {
  383. kind: 'dropdown',
  384. options: CONDITIONS_ARGUMENTS,
  385. dataType: 'string',
  386. defaultValue: CONDITIONS_ARGUMENTS[0].value,
  387. required: true,
  388. },
  389. {
  390. kind: 'value',
  391. dataType: 'string',
  392. defaultValue: '300',
  393. required: true,
  394. },
  395. ],
  396. outputType: 'number',
  397. isSortable: true,
  398. multiPlotType: 'area',
  399. },
  400. } as const;
  401. // TPM and TPS are aliases that are only used in Performance
  402. export const ALIASES = {
  403. tpm: 'epm',
  404. tps: 'eps',
  405. };
  406. assert(AGGREGATIONS as Readonly<{[key in keyof typeof AGGREGATIONS]: Aggregation}>);
  407. export type AggregationKey = keyof typeof AGGREGATIONS | keyof typeof ALIASES | '';
  408. export type AggregationOutputType = Extract<
  409. ColumnType,
  410. 'number' | 'integer' | 'date' | 'duration' | 'percentage' | 'string'
  411. >;
  412. export type PlotType = 'bar' | 'line' | 'area';
  413. type DefaultValueInputs = {
  414. parameter: AggregateParameter;
  415. organization: LightWeightOrganization;
  416. };
  417. export type Aggregation = {
  418. /**
  419. * List of parameters for the function.
  420. */
  421. parameters: Readonly<AggregateParameter[]>;
  422. /**
  423. * The output type. Null means to inherit from the field.
  424. */
  425. outputType: AggregationOutputType | null;
  426. /**
  427. * Can this function be used in a sort result
  428. */
  429. isSortable: boolean;
  430. /**
  431. * How this function should be plotted when shown in a multiseries result (top5)
  432. * Optional because some functions cannot be plotted (strings/dates)
  433. */
  434. multiPlotType?: PlotType;
  435. getFieldOverrides?: (
  436. data: DefaultValueInputs
  437. ) => Partial<Omit<AggregateParameter, 'kind'>>;
  438. };
  439. enum FieldKey {
  440. CULPRIT = 'culprit',
  441. DEVICE_ARCH = 'device.arch',
  442. DEVICE_BATTERY_LEVEL = 'device.battery_level',
  443. DEVICE_BRAND = 'device.brand',
  444. DEVICE_CHARGING = 'device.charging',
  445. DEVICE_LOCALE = 'device.locale',
  446. DEVICE_NAME = 'device.name',
  447. DEVICE_ONLINE = 'device.online',
  448. DEVICE_ORIENTATION = 'device.orientation',
  449. DEVICE_SIMULATOR = 'device.simulator',
  450. DEVICE_UUID = 'device.uuid',
  451. DIST = 'dist',
  452. ENVIRONMENT = 'environment',
  453. ERROR_HANDLED = 'error.handled',
  454. ERROR_UNHANDLED = 'error.unhandled',
  455. ERROR_MECHANISM = 'error.mechanism',
  456. ERROR_TYPE = 'error.type',
  457. ERROR_VALUE = 'error.value',
  458. EVENT_TYPE = 'event.type',
  459. GEO_CITY = 'geo.city',
  460. GEO_COUNTRY_CODE = 'geo.country_code',
  461. GEO_REGION = 'geo.region',
  462. HTTP_METHOD = 'http.method',
  463. HTTP_REFERER = 'http.referer',
  464. HTTP_URL = 'http.url',
  465. ID = 'id',
  466. ISSUE = 'issue',
  467. LOCATION = 'location',
  468. MESSAGE = 'message',
  469. OS_BUILD = 'os.build',
  470. OS_KERNEL_VERSION = 'os.kernel_version',
  471. PLATFORM_NAME = 'platform.name',
  472. PROJECT = 'project',
  473. RELEASE = 'release',
  474. SDK_NAME = 'sdk.name',
  475. SDK_VERSION = 'sdk.version',
  476. STACK_ABS_PATH = 'stack.abs_path',
  477. STACK_COLNO = 'stack.colno',
  478. STACK_FILENAME = 'stack.filename',
  479. STACK_FUNCTION = 'stack.function',
  480. STACK_IN_APP = 'stack.in_app',
  481. STACK_LINENO = 'stack.lineno',
  482. STACK_MODULE = 'stack.module',
  483. STACK_PACKAGE = 'stack.package',
  484. STACK_STACK_LEVEL = 'stack.stack_level',
  485. TIMESTAMP = 'timestamp',
  486. TIMESTAMP_TO_HOUR = 'timestamp.to_hour',
  487. TIMESTAMP_TO_DAY = 'timestamp.to_day',
  488. TITLE = 'title',
  489. TRACE = 'trace',
  490. TRACE_PARENT_SPAN = 'trace.parent_span',
  491. TRACE_SPAN = 'trace.span',
  492. TRANSACTION = 'transaction',
  493. TRANSACTION_DURATION = 'transaction.duration',
  494. TRANSACTION_OP = 'transaction.op',
  495. TRANSACTION_STATUS = 'transaction.status',
  496. USER_EMAIL = 'user.email',
  497. USER_ID = 'user.id',
  498. USER_IP = 'user.ip',
  499. USER_USERNAME = 'user.username',
  500. USER_DISPLAY = 'user.display',
  501. }
  502. /**
  503. * Refer to src/sentry/snuba/events.py, search for Columns
  504. */
  505. export const FIELDS: Readonly<Record<FieldKey, ColumnType>> = {
  506. [FieldKey.ID]: 'string',
  507. // issue.id and project.id are omitted on purpose.
  508. // Customers should use `issue` and `project` instead.
  509. [FieldKey.TIMESTAMP]: 'date',
  510. // time is omitted on purpose.
  511. // Customers should use `timestamp` or `timestamp.to_hour`.
  512. [FieldKey.TIMESTAMP_TO_HOUR]: 'date',
  513. [FieldKey.TIMESTAMP_TO_DAY]: 'date',
  514. [FieldKey.CULPRIT]: 'string',
  515. [FieldKey.LOCATION]: 'string',
  516. [FieldKey.MESSAGE]: 'string',
  517. [FieldKey.PLATFORM_NAME]: 'string',
  518. [FieldKey.ENVIRONMENT]: 'string',
  519. [FieldKey.RELEASE]: 'string',
  520. [FieldKey.DIST]: 'string',
  521. [FieldKey.TITLE]: 'string',
  522. [FieldKey.EVENT_TYPE]: 'string',
  523. // tags.key and tags.value are omitted on purpose as well.
  524. [FieldKey.TRANSACTION]: 'string',
  525. [FieldKey.USER_ID]: 'string',
  526. [FieldKey.USER_EMAIL]: 'string',
  527. [FieldKey.USER_USERNAME]: 'string',
  528. [FieldKey.USER_IP]: 'string',
  529. [FieldKey.SDK_NAME]: 'string',
  530. [FieldKey.SDK_VERSION]: 'string',
  531. [FieldKey.HTTP_METHOD]: 'string',
  532. [FieldKey.HTTP_REFERER]: 'string',
  533. [FieldKey.HTTP_URL]: 'string',
  534. [FieldKey.OS_BUILD]: 'string',
  535. [FieldKey.OS_KERNEL_VERSION]: 'string',
  536. [FieldKey.DEVICE_NAME]: 'string',
  537. [FieldKey.DEVICE_BRAND]: 'string',
  538. [FieldKey.DEVICE_LOCALE]: 'string',
  539. [FieldKey.DEVICE_UUID]: 'string',
  540. [FieldKey.DEVICE_ARCH]: 'string',
  541. [FieldKey.DEVICE_BATTERY_LEVEL]: 'number',
  542. [FieldKey.DEVICE_ORIENTATION]: 'string',
  543. [FieldKey.DEVICE_SIMULATOR]: 'boolean',
  544. [FieldKey.DEVICE_ONLINE]: 'boolean',
  545. [FieldKey.DEVICE_CHARGING]: 'boolean',
  546. [FieldKey.GEO_COUNTRY_CODE]: 'string',
  547. [FieldKey.GEO_REGION]: 'string',
  548. [FieldKey.GEO_CITY]: 'string',
  549. [FieldKey.ERROR_TYPE]: 'string',
  550. [FieldKey.ERROR_VALUE]: 'string',
  551. [FieldKey.ERROR_MECHANISM]: 'string',
  552. [FieldKey.ERROR_HANDLED]: 'boolean',
  553. [FieldKey.ERROR_UNHANDLED]: 'boolean',
  554. [FieldKey.STACK_ABS_PATH]: 'string',
  555. [FieldKey.STACK_FILENAME]: 'string',
  556. [FieldKey.STACK_PACKAGE]: 'string',
  557. [FieldKey.STACK_MODULE]: 'string',
  558. [FieldKey.STACK_FUNCTION]: 'string',
  559. [FieldKey.STACK_IN_APP]: 'boolean',
  560. [FieldKey.STACK_COLNO]: 'number',
  561. [FieldKey.STACK_LINENO]: 'number',
  562. [FieldKey.STACK_STACK_LEVEL]: 'number',
  563. // contexts.key and contexts.value omitted on purpose.
  564. // Transaction event fields.
  565. [FieldKey.TRANSACTION_DURATION]: 'duration',
  566. [FieldKey.TRANSACTION_OP]: 'string',
  567. [FieldKey.TRANSACTION_STATUS]: 'string',
  568. [FieldKey.TRACE]: 'string',
  569. [FieldKey.TRACE_SPAN]: 'string',
  570. [FieldKey.TRACE_PARENT_SPAN]: 'string',
  571. // Field alises defined in src/sentry/api/event_search.py
  572. [FieldKey.PROJECT]: 'string',
  573. [FieldKey.ISSUE]: 'string',
  574. [FieldKey.USER_DISPLAY]: 'string',
  575. };
  576. export type FieldTag = {
  577. key: FieldKey;
  578. name: FieldKey;
  579. };
  580. export const FIELD_TAGS = Object.freeze(
  581. Object.fromEntries(Object.keys(FIELDS).map(item => [item, {key: item, name: item}]))
  582. );
  583. // Allows for a less strict field key definition in cases we are returning custom strings as fields
  584. export type LooseFieldKey = FieldKey | string | '';
  585. export enum WebVital {
  586. FP = 'measurements.fp',
  587. FCP = 'measurements.fcp',
  588. LCP = 'measurements.lcp',
  589. FID = 'measurements.fid',
  590. CLS = 'measurements.cls',
  591. TTFB = 'measurements.ttfb',
  592. RequestTime = 'measurements.ttfb.requesttime',
  593. }
  594. const MEASUREMENTS: Readonly<Record<WebVital, ColumnType>> = {
  595. [WebVital.FP]: 'duration',
  596. [WebVital.FCP]: 'duration',
  597. [WebVital.LCP]: 'duration',
  598. [WebVital.FID]: 'duration',
  599. [WebVital.CLS]: 'number',
  600. [WebVital.TTFB]: 'duration',
  601. [WebVital.RequestTime]: 'duration',
  602. };
  603. // This list contains fields/functions that are available with performance-view feature.
  604. export const TRACING_FIELDS = [
  605. 'avg',
  606. 'sum',
  607. 'transaction.duration',
  608. 'transaction.op',
  609. 'transaction.status',
  610. 'p50',
  611. 'p75',
  612. 'p95',
  613. 'p99',
  614. 'p100',
  615. 'percentile',
  616. 'failure_rate',
  617. 'apdex',
  618. 'count_miserable',
  619. 'user_misery',
  620. 'eps',
  621. 'epm',
  622. 'key_transaction',
  623. 'team_key_transaction',
  624. ...Object.keys(MEASUREMENTS),
  625. ];
  626. export const MEASUREMENT_PATTERN = /^measurements\.([a-zA-Z0-9-_.]+)$/;
  627. export const SPAN_OP_BREAKDOWN_PATTERN = /^spans\.([a-zA-Z0-9-_.]+)$/;
  628. export function isMeasurement(field: string): boolean {
  629. const results = field.match(MEASUREMENT_PATTERN);
  630. return !!results;
  631. }
  632. export function measurementType(field: string) {
  633. if (MEASUREMENTS.hasOwnProperty(field)) {
  634. return MEASUREMENTS[field];
  635. }
  636. return 'number';
  637. }
  638. export function getMeasurementSlug(field: string): string | null {
  639. const results = field.match(MEASUREMENT_PATTERN);
  640. if (results && results.length >= 2) {
  641. return results[1];
  642. }
  643. return null;
  644. }
  645. const AGGREGATE_PATTERN = /^([^\(]+)\((.*)?\)$/;
  646. // Identical to AGGREGATE_PATTERN, but without the $ for newline, or ^ for start of line
  647. const AGGREGATE_BASE = /([^\(]+)\((.*)?\)/g;
  648. export function getAggregateArg(field: string): string | null {
  649. // only returns the first argument if field is an aggregate
  650. const result = parseFunction(field);
  651. if (result && result.arguments.length > 0) {
  652. return result.arguments[0];
  653. }
  654. return null;
  655. }
  656. export function parseFunction(field: string): ParsedFunction | null {
  657. const results = field.match(AGGREGATE_PATTERN);
  658. if (results && results.length === 3) {
  659. return {
  660. name: results[1],
  661. arguments: parseArguments(results[1], results[2]),
  662. };
  663. }
  664. return null;
  665. }
  666. export function parseArguments(functionText: string, columnText: string): string[] {
  667. // Some functions take a quoted string for their arguments that may contain commas
  668. // This function attempts to be identical with the similarly named parse_arguments
  669. // found in src/sentry/search/events/fields.py
  670. if (
  671. (functionText !== 'to_other' && functionText !== 'count_if') ||
  672. columnText.length === 0
  673. ) {
  674. return columnText ? columnText.split(',').map(result => result.trim()) : [];
  675. }
  676. const args: string[] = [];
  677. let quoted = false;
  678. let escaped = false;
  679. let i: number = 0;
  680. let j: number = 0;
  681. while (j < columnText.length) {
  682. if (i === j && columnText[j] === '"') {
  683. // when we see a quote at the beginning of
  684. // an argument, then this is a quoted string
  685. quoted = true;
  686. } else if (quoted && !escaped && columnText[j] === '\\') {
  687. // when we see a slash inside a quoted string,
  688. // the next character is an escape character
  689. escaped = true;
  690. } else if (quoted && !escaped && columnText[j] === '"') {
  691. // when we see a non-escaped quote while inside
  692. // of a quoted string, we should end it
  693. quoted = false;
  694. } else if (quoted && escaped) {
  695. // when we are inside a quoted string and have
  696. // begun an escape character, we should end it
  697. escaped = false;
  698. } else if (quoted && columnText[j] === ',') {
  699. // when we are inside a quoted string and see
  700. // a comma, it should not be considered an
  701. // argument separator
  702. } else if (columnText[j] === ',') {
  703. // when we see a comma outside of a quoted string
  704. // it is an argument separator
  705. args.push(columnText.substring(i, j).trim());
  706. i = j + 1;
  707. }
  708. j += 1;
  709. }
  710. if (i !== j) {
  711. // add in the last argument if any
  712. args.push(columnText.substring(i).trim());
  713. }
  714. return args;
  715. }
  716. // `|` is an invalid field character, so it is used to determine whether a field is an equation or not
  717. const EQUATION_PREFIX = 'equation|';
  718. const EQUATION_ALIAS_PATTERN = /^equation\[(\d+)\]$/;
  719. export function isEquation(field: string): boolean {
  720. return field.startsWith(EQUATION_PREFIX);
  721. }
  722. export function isEquationAlias(field: string): boolean {
  723. return EQUATION_ALIAS_PATTERN.test(field);
  724. }
  725. export function getEquationAliasIndex(field: string): number {
  726. const results = field.match(EQUATION_ALIAS_PATTERN);
  727. if (results && results.length === 2) {
  728. return parseInt(results[1], 10);
  729. }
  730. return -1;
  731. }
  732. export function getEquation(field: string): string {
  733. return field.slice(EQUATION_PREFIX.length);
  734. }
  735. export function isAggregateEquation(field: string): boolean {
  736. const results = field.match(AGGREGATE_BASE);
  737. return isEquation(field) && results !== null && results.length > 0;
  738. }
  739. export function generateAggregateFields(
  740. organization: LightWeightOrganization,
  741. eventFields: readonly Field[] | Field[],
  742. excludeFields: readonly string[] = []
  743. ): Field[] {
  744. const functions = Object.keys(AGGREGATIONS);
  745. const fields = Object.values(eventFields).map(field => field.field);
  746. functions.forEach(func => {
  747. const parameters = AGGREGATIONS[func].parameters.map(param => {
  748. const overrides = AGGREGATIONS[func].getFieldOverrides;
  749. if (typeof overrides === 'undefined') {
  750. return param;
  751. }
  752. return {
  753. ...param,
  754. ...overrides({parameter: param, organization}),
  755. };
  756. });
  757. if (parameters.every(param => typeof param.defaultValue !== 'undefined')) {
  758. const newField = `${func}(${parameters
  759. .map(param => param.defaultValue)
  760. .join(',')})`;
  761. if (fields.indexOf(newField) === -1 && excludeFields.indexOf(newField) === -1) {
  762. fields.push(newField);
  763. }
  764. }
  765. });
  766. return fields.map(field => ({field})) as Field[];
  767. }
  768. export function explodeFieldString(field: string): Column {
  769. if (isEquation(field)) {
  770. return {kind: 'equation', field: getEquation(field)};
  771. }
  772. const results = parseFunction(field);
  773. if (results) {
  774. return {
  775. kind: 'function',
  776. function: [
  777. results.name as AggregationKey,
  778. results.arguments[0] ?? '',
  779. results.arguments[1] as AggregationRefinement,
  780. results.arguments[2] as AggregationRefinement,
  781. ],
  782. };
  783. }
  784. return {kind: 'field', field};
  785. }
  786. export function generateFieldAsString(value: QueryFieldValue): string {
  787. if (value.kind === 'field') {
  788. return value.field;
  789. } else if (value.kind === 'equation') {
  790. return `${EQUATION_PREFIX}${value.field}`;
  791. }
  792. const aggregation = value.function[0];
  793. const parameters = value.function.slice(1).filter(i => i);
  794. return `${aggregation}(${parameters.join(',')})`;
  795. }
  796. export function explodeField(field: Field): Column {
  797. const results = explodeFieldString(field.field);
  798. return results;
  799. }
  800. /**
  801. * Get the alias that the API results will have for a given aggregate function name
  802. */
  803. export function getAggregateAlias(field: string): string {
  804. const result = parseFunction(field);
  805. if (!result) {
  806. return field;
  807. }
  808. let alias = result.name;
  809. if (result.arguments.length > 0) {
  810. alias += '_' + result.arguments.join('_');
  811. }
  812. return alias.replace(/[^\w]/g, '_').replace(/^_+/g, '').replace(/_+$/, '');
  813. }
  814. /**
  815. * Check if a field name looks like an aggregate function or known aggregate alias.
  816. */
  817. export function isAggregateField(field: string): boolean {
  818. return parseFunction(field) !== null;
  819. }
  820. /**
  821. * Convert a function string into type it will output.
  822. * This is useful when you need to format values in tooltips,
  823. * or in series markers.
  824. */
  825. export function aggregateOutputType(field: string): AggregationOutputType {
  826. const result = parseFunction(field);
  827. if (!result) {
  828. return 'number';
  829. }
  830. const outputType = aggregateFunctionOutputType(result.name, result.arguments[0]);
  831. if (outputType === null) {
  832. return 'number';
  833. }
  834. return outputType;
  835. }
  836. /**
  837. * Converts a function string and its first argument into its output type.
  838. * - If the function has a fixed output type, that will be the result.
  839. * - If the function does not define an output type, the output type will be equal to
  840. * the type of its first argument.
  841. * - If the function has an optional first argument, and it was not defined, make sure
  842. * to use the default argument as the first argument.
  843. * - If the type could not be determined, return null.
  844. */
  845. export function aggregateFunctionOutputType(
  846. funcName: string,
  847. firstArg: string | undefined
  848. ): AggregationOutputType | null {
  849. const aggregate = AGGREGATIONS[ALIASES[funcName] || funcName];
  850. // Attempt to use the function's outputType.
  851. if (aggregate?.outputType) {
  852. return aggregate.outputType;
  853. }
  854. // If the first argument is undefined and it is not required,
  855. // then we attempt to get the default value.
  856. if (!firstArg && aggregate?.parameters?.[0]) {
  857. if (aggregate.parameters[0].required === false) {
  858. firstArg = aggregate.parameters[0].defaultValue;
  859. }
  860. }
  861. // If the function is an inherit type it will have a field as
  862. // the first parameter and we can use that to get the type.
  863. if (firstArg && FIELDS.hasOwnProperty(firstArg)) {
  864. return FIELDS[firstArg];
  865. } else if (firstArg && isMeasurement(firstArg)) {
  866. return measurementType(firstArg);
  867. } else if (firstArg && isSpanOperationBreakdownField(firstArg)) {
  868. return 'duration';
  869. }
  870. return null;
  871. }
  872. /**
  873. * Get the multi-series chart type for an aggregate function.
  874. */
  875. export function aggregateMultiPlotType(field: string): PlotType {
  876. const result = parseFunction(field);
  877. // Handle invalid data.
  878. if (!result) {
  879. return 'area';
  880. }
  881. if (!AGGREGATIONS.hasOwnProperty(result.name)) {
  882. return 'area';
  883. }
  884. return AGGREGATIONS[result.name].multiPlotType;
  885. }
  886. function validateForNumericAggregate(
  887. validColumnTypes: ColumnType[]
  888. ): ValidateColumnValueFunction {
  889. return function ({name, dataType}: {name: string; dataType: ColumnType}): boolean {
  890. // these built-in columns cannot be applied to numeric aggregates such as percentile(...)
  891. if (
  892. [
  893. FieldKey.DEVICE_BATTERY_LEVEL,
  894. FieldKey.STACK_COLNO,
  895. FieldKey.STACK_LINENO,
  896. FieldKey.STACK_STACK_LEVEL,
  897. ].includes(name as FieldKey)
  898. ) {
  899. return false;
  900. }
  901. return validColumnTypes.includes(dataType);
  902. };
  903. }
  904. function validateAllowedColumns(validColumns: string[]): ValidateColumnValueFunction {
  905. return function ({name}): boolean {
  906. return validColumns.includes(name);
  907. };
  908. }
  909. const alignedTypes: ColumnValueType[] = ['number', 'duration', 'integer', 'percentage'];
  910. export function fieldAlignment(
  911. columnName: string,
  912. columnType?: undefined | ColumnValueType,
  913. metadata?: Record<string, ColumnValueType>
  914. ): Alignments {
  915. let align: Alignments = 'left';
  916. if (columnType) {
  917. align = alignedTypes.includes(columnType) ? 'right' : 'left';
  918. }
  919. if (columnType === undefined || columnType === 'never') {
  920. // fallback to align the column based on the table metadata
  921. const maybeType = metadata ? metadata[getAggregateAlias(columnName)] : undefined;
  922. if (maybeType !== undefined && alignedTypes.includes(maybeType)) {
  923. align = 'right';
  924. }
  925. }
  926. return align;
  927. }
  928. /**
  929. * Match on types that are legal to show on a timeseries chart.
  930. */
  931. export function isLegalYAxisType(match: ColumnType) {
  932. return ['number', 'integer', 'duration', 'percentage'].includes(match);
  933. }
  934. export function isSpanOperationBreakdownField(field: string) {
  935. return field.startsWith('spans.');
  936. }
  937. export const SPAN_OP_RELATIVE_BREAKDOWN_FIELD = 'span_ops_breakdown.relative';
  938. export function isRelativeSpanOperationBreakdownField(field: string) {
  939. return field === SPAN_OP_RELATIVE_BREAKDOWN_FIELD;
  940. }
  941. export const SPAN_OP_BREAKDOWN_FIELDS = [
  942. 'spans.http',
  943. 'spans.db',
  944. 'spans.browser',
  945. 'spans.resource',
  946. ];
  947. export function getSpanOperationName(field: string): string | null {
  948. const results = field.match(SPAN_OP_BREAKDOWN_PATTERN);
  949. if (results && results.length >= 2) {
  950. return results[1];
  951. }
  952. return null;
  953. }
  954. export function getColumnType(column: Column): ColumnType {
  955. if (column.kind === 'function') {
  956. const outputType = aggregateFunctionOutputType(
  957. column.function[0],
  958. column.function[1]
  959. );
  960. if (outputType !== null) {
  961. return outputType;
  962. }
  963. } else if (column.kind === 'field') {
  964. if (FIELDS.hasOwnProperty(column.field)) {
  965. return FIELDS[column.field];
  966. } else if (isMeasurement(column.field)) {
  967. return measurementType(column.field);
  968. } else if (isSpanOperationBreakdownField(column.field)) {
  969. return 'duration';
  970. }
  971. }
  972. return 'string';
  973. }