metrics.tsx 9.4 KB

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