metrics.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. import omit from 'lodash/omit';
  2. import {Client, ResponseMeta} from 'sentry/api';
  3. import {t} from 'sentry/locale';
  4. import {MetricsApiResponse, Organization, PageFilters, TagCollection} from 'sentry/types';
  5. import {Series} from 'sentry/types/echarts';
  6. import {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements';
  7. import {TableData} from 'sentry/utils/discover/discoverQuery';
  8. import {EventData} from 'sentry/utils/discover/eventView';
  9. import {NumberContainer} from 'sentry/utils/discover/styles';
  10. import {
  11. formatMetricUsingUnit,
  12. getMetricsApiRequestQuery,
  13. getSeriesName,
  14. groupByOp,
  15. } from 'sentry/utils/metrics';
  16. import {
  17. formatMRIField,
  18. getMRI,
  19. getUseCaseFromMRI,
  20. isMRIField,
  21. parseField,
  22. parseMRI,
  23. } from 'sentry/utils/metrics/mri';
  24. import {OnDemandControlContext} from 'sentry/utils/performance/contexts/onDemandControl';
  25. import {MetricSearchBar} from 'sentry/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/metricSearchBar';
  26. import {FieldValueOption} from 'sentry/views/discover/table/queryField';
  27. import {FieldValueKind} from 'sentry/views/discover/table/types';
  28. import {DisplayType, Widget, WidgetQuery} from '../types';
  29. import {DatasetConfig, handleOrderByReset} from './base';
  30. const DEFAULT_WIDGET_QUERY: WidgetQuery = {
  31. name: '',
  32. fields: [''],
  33. columns: [''],
  34. fieldAliases: [],
  35. aggregates: [''],
  36. conditions: '',
  37. orderby: '',
  38. };
  39. export const MetricsConfig: DatasetConfig<MetricsApiResponse, MetricsApiResponse> = {
  40. defaultWidgetQuery: DEFAULT_WIDGET_QUERY,
  41. enableEquations: false,
  42. getTableRequest: (
  43. api: Client,
  44. _: Widget,
  45. query: WidgetQuery,
  46. organization: Organization,
  47. pageFilters: PageFilters,
  48. __?: OnDemandControlContext,
  49. limit?: number
  50. ) => getMetricRequest(api, query, organization, pageFilters, limit),
  51. getSeriesRequest: getMetricSeriesRequest,
  52. getCustomFieldRenderer: field => (data: EventData) =>
  53. renderMetricField(field, data[field]),
  54. SearchBar: MetricSearchBar,
  55. handleOrderByReset: handleMetricTableOrderByReset,
  56. supportedDisplayTypes: [
  57. DisplayType.AREA,
  58. DisplayType.BAR,
  59. DisplayType.BIG_NUMBER,
  60. DisplayType.LINE,
  61. DisplayType.TABLE,
  62. DisplayType.TOP_N,
  63. ],
  64. transformSeries: transformMetricsResponseToSeries,
  65. transformTable: transformMetricsResponseToTable,
  66. getTableFieldOptions: getFields,
  67. getTimeseriesSortOptions: getMetricTimeseriesSortOptions,
  68. getTableSortOptions: getMetricTableSortOptions,
  69. filterTableOptions: filterMetricOperations,
  70. filterYAxisOptions: () => option => filterMetricOperations(option),
  71. filterAggregateParams: filterMetricMRIs,
  72. filterYAxisAggregateParams: () => option => filterMetricMRIs(option),
  73. getGroupByFieldOptions: getTagsForMetric,
  74. getFieldHeaderMap: getFormattedMRIHeaders,
  75. };
  76. export function renderMetricField(field: string, value: any) {
  77. const parsedField = parseField(field);
  78. if (parsedField) {
  79. const unit = parseMRI(parsedField.mri)?.unit ?? '';
  80. return <NumberContainer>{formatMetricUsingUnit(value, unit)}</NumberContainer>;
  81. }
  82. return value;
  83. }
  84. export function formatMetricAxisValue(field: string, value: number) {
  85. const unit = parseMRI(parseField(field)?.mri)?.unit ?? '';
  86. return formatMetricUsingUnit(value, unit);
  87. }
  88. function getFormattedMRIHeaders(query?: WidgetQuery) {
  89. if (!query) {
  90. return {};
  91. }
  92. return (query.fields || []).reduce((acc, field, index) => {
  93. const fieldAlias = query.fieldAliases?.[index];
  94. acc[field] = fieldAlias || formatMRIField(field);
  95. return acc;
  96. }, {});
  97. }
  98. function getMetricTimeseriesSortOptions(_, widgetQuery: WidgetQuery) {
  99. if (!widgetQuery.fields?.[0]) {
  100. return [];
  101. }
  102. return widgetQuery.fields.reduce((acc, field) => {
  103. if (!isMRIField(field)) {
  104. return acc;
  105. }
  106. return {
  107. ...acc,
  108. [`field:${field}`]: {
  109. label: formatMRIField(field),
  110. value: {
  111. kind: FieldValueKind.FIELD,
  112. value: field,
  113. meta: {
  114. name: field,
  115. dataType: 'number',
  116. },
  117. },
  118. },
  119. };
  120. }, {});
  121. }
  122. function getMetricTableSortOptions(_, widgetQuery: WidgetQuery) {
  123. if (!widgetQuery.fields?.[0]) {
  124. return [];
  125. }
  126. return widgetQuery.fields
  127. .map((field, i) => {
  128. const alias = widgetQuery.fieldAliases?.[i];
  129. return {
  130. label: alias || formatMRIField(field),
  131. value: field,
  132. };
  133. })
  134. .filter(option => isMRIField(option.value));
  135. }
  136. function getFields(
  137. organization: Organization,
  138. _?: TagCollection | undefined,
  139. __?: CustomMeasurementCollection,
  140. api?: Client
  141. ) {
  142. if (!api) {
  143. return {};
  144. }
  145. return api
  146. .requestPromise(`/organizations/${organization.slug}/metrics/meta/`, {
  147. query: {useCase: 'custom'},
  148. })
  149. .then(metaReponse => {
  150. const groupedByOp = groupByOp(metaReponse);
  151. const typesByOp: Record<string, Set<string>> = Object.entries(groupedByOp).reduce(
  152. (acc, [operation, fields]) => {
  153. const types = new Set();
  154. fields.forEach(field => types.add(field.type));
  155. acc[operation] = types;
  156. return acc;
  157. },
  158. {}
  159. );
  160. const fieldOptions: Record<string, any> = {};
  161. Object.entries(groupedByOp).forEach(([operation, fields]) => {
  162. fieldOptions[`function:${operation}`] = {
  163. label: `${operation}(${'\u2026'})`,
  164. value: {
  165. kind: FieldValueKind.FUNCTION,
  166. meta: {
  167. name: operation,
  168. parameters: [
  169. {
  170. kind: 'column',
  171. columnTypes: [...typesByOp[operation]],
  172. defaultValue: fields[0].mri,
  173. required: true,
  174. },
  175. ],
  176. },
  177. },
  178. };
  179. });
  180. metaReponse
  181. .sort((a, b) => a.name.localeCompare(b.name))
  182. .forEach(field => {
  183. fieldOptions[`field:${field.mri}`] = {
  184. label: field.name,
  185. value: {
  186. kind: FieldValueKind.METRICS,
  187. meta: {
  188. name: field.mri,
  189. dataType: field.type,
  190. },
  191. },
  192. };
  193. });
  194. return fieldOptions;
  195. });
  196. }
  197. function filterMetricOperations(option: FieldValueOption) {
  198. return option.value.kind === FieldValueKind.FUNCTION;
  199. }
  200. function filterMetricMRIs(option: FieldValueOption) {
  201. return option.value.kind === FieldValueKind.METRICS;
  202. }
  203. function getTagsForMetric(
  204. organization: Organization,
  205. _?: TagCollection,
  206. __?: CustomMeasurementCollection,
  207. api?: Client,
  208. queries?: WidgetQuery[]
  209. ) {
  210. const fieldOptions = {};
  211. if (!api) {
  212. return fieldOptions;
  213. }
  214. const field = queries?.[0].aggregates[0] ?? '';
  215. const mri = getMRI(field);
  216. const useCase = getUseCaseFromMRI(mri);
  217. return api
  218. .requestPromise(`/organizations/${organization.slug}/metrics/tags/`, {
  219. query: {metric: mri, useCase},
  220. })
  221. .then(tagsResponse => {
  222. tagsResponse.forEach(tag => {
  223. fieldOptions[`field:${tag.key}`] = {
  224. label: tag.key,
  225. value: {
  226. kind: FieldValueKind.TAG,
  227. meta: {name: tag.key, dataType: 'string'},
  228. },
  229. };
  230. });
  231. return fieldOptions;
  232. });
  233. }
  234. function getMetricSeriesRequest(
  235. api: Client,
  236. widget: Widget,
  237. queryIndex: number,
  238. organization: Organization,
  239. pageFilters: PageFilters
  240. ) {
  241. const query = widget.queries[queryIndex];
  242. return getMetricRequest(
  243. api,
  244. query,
  245. organization,
  246. pageFilters,
  247. widget.limit,
  248. widget.displayType
  249. );
  250. }
  251. function handleMetricTableOrderByReset(widgetQuery: WidgetQuery, newFields: string[]) {
  252. const disableSortBy = widgetQuery.columns.includes('session.status');
  253. if (disableSortBy) {
  254. widgetQuery.orderby = '';
  255. }
  256. return handleOrderByReset(widgetQuery, newFields);
  257. }
  258. export function transformMetricsResponseToTable(data: MetricsApiResponse): TableData {
  259. const rows = data.groups.map((group, index) => {
  260. const groupColumn = mapMetricGroupsToFields(group.by);
  261. const value = mapMetricGroupsToFields(group.totals);
  262. return {
  263. id: String(index),
  264. ...groupColumn,
  265. ...value,
  266. };
  267. });
  268. const singleRow = rows[0];
  269. const meta = {
  270. ...changeObjectValuesToTypes(omit(singleRow, 'id')),
  271. };
  272. return {meta, data: rows};
  273. }
  274. function mapMetricGroupsToFields(
  275. results: Record<string, number | string | null> | undefined
  276. ) {
  277. if (!results) {
  278. return {};
  279. }
  280. const mappedResults: typeof results = {};
  281. for (const [key, value] of Object.entries(results)) {
  282. mappedResults[key] = value;
  283. }
  284. return mappedResults;
  285. }
  286. function changeObjectValuesToTypes(
  287. obj: Record<string, number | string | null> | undefined
  288. ) {
  289. return Object.entries(obj ?? {}).reduce((acc, [key, value]) => {
  290. acc[key] = typeof value;
  291. return acc;
  292. }, {});
  293. }
  294. export function transformMetricsResponseToSeries(
  295. data: MetricsApiResponse,
  296. widgetQuery: WidgetQuery
  297. ) {
  298. if (data === null) {
  299. return [];
  300. }
  301. const results: Series[] = [];
  302. const queryAlias = widgetQuery.name;
  303. if (!data.groups.length) {
  304. return [
  305. {
  306. seriesName: `(${t('no results')})`,
  307. data: data.intervals.map(interval => ({
  308. name: interval,
  309. value: 0,
  310. })),
  311. },
  312. ];
  313. }
  314. data.groups.forEach(group => {
  315. Object.keys(group.series).forEach(field => {
  316. results.push({
  317. seriesName:
  318. queryAlias ||
  319. getSeriesName(group, data.groups.length === 1, widgetQuery.columns),
  320. data: data.intervals.map((interval, index) => ({
  321. name: interval,
  322. value: group.series[field][index] ?? 0,
  323. })),
  324. });
  325. });
  326. });
  327. return results;
  328. }
  329. function getMetricRequest(
  330. api: Client,
  331. query: WidgetQuery,
  332. organization: Organization,
  333. pageFilters: PageFilters,
  334. limit?: number,
  335. displayType?: DisplayType
  336. ): Promise<[MetricsApiResponse, string | undefined, ResponseMeta | undefined]> {
  337. if (!query.aggregates[0]) {
  338. // No aggregate selected, return empty response
  339. return Promise.resolve([
  340. {
  341. intervals: [],
  342. groups: [],
  343. meta: [],
  344. },
  345. 'OK',
  346. {
  347. getResponseHeader: () => '',
  348. },
  349. ] as any);
  350. }
  351. const useNewMetricsLayer = organization.features.includes(
  352. 'metrics-api-new-metrics-layer'
  353. );
  354. const requestData = getMetricsApiRequestQuery(
  355. {
  356. field: query.aggregates[0],
  357. query: query.conditions || undefined,
  358. groupBy: query.columns || undefined,
  359. orderBy: query.orderby || undefined,
  360. },
  361. pageFilters,
  362. {
  363. limit: limit || undefined,
  364. useNewMetricsLayer,
  365. fidelity: displayType === DisplayType.BAR ? 'low' : 'high',
  366. }
  367. );
  368. const pathname = `/organizations/${organization.slug}/metrics/data/`;
  369. return api.requestPromise(pathname, {
  370. includeAllArgs: true,
  371. query: requestData,
  372. });
  373. }