metrics.tsx 9.9 KB

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