metrics.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  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. formatMRI,
  18. formatMRIField,
  19. getMRI,
  20. getUseCaseFromMRI,
  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) {
  99. if (!widgetQuery.columns) {
  100. return [];
  101. }
  102. return widgetQuery.columns.reduce((acc, column) => {
  103. return {
  104. ...acc,
  105. [column]: {
  106. label: column,
  107. value: {
  108. kind: FieldValueKind.TAG,
  109. meta: {
  110. name: column,
  111. dataType: 'string',
  112. },
  113. },
  114. },
  115. };
  116. }, {});
  117. }
  118. function getMetricTableSortOptions(_, widgetQuery) {
  119. if (!widgetQuery.fields[0]) {
  120. return [];
  121. }
  122. return widgetQuery.fields.map((field, i) => {
  123. const mri = getMRI(field);
  124. const alias = widgetQuery.fieldAliases?.[i];
  125. return {
  126. label: alias ?? formatMRI(mri),
  127. value: mri,
  128. };
  129. });
  130. }
  131. function getFields(
  132. organization: Organization,
  133. _?: TagCollection | undefined,
  134. __?: CustomMeasurementCollection,
  135. api?: Client
  136. ) {
  137. if (!api) {
  138. return {};
  139. }
  140. return api
  141. .requestPromise(`/organizations/${organization.slug}/metrics/meta/`, {
  142. query: {useCase: 'custom'},
  143. })
  144. .then(metaReponse => {
  145. const groupedByOp = groupByOp(metaReponse);
  146. const typesByOp: Record<string, Set<string>> = Object.entries(groupedByOp).reduce(
  147. (acc, [operation, fields]) => {
  148. const types = new Set();
  149. fields.forEach(field => types.add(field.type));
  150. acc[operation] = types;
  151. return acc;
  152. },
  153. {}
  154. );
  155. const fieldOptions: Record<string, any> = {};
  156. Object.entries(groupedByOp).forEach(([operation, fields]) => {
  157. fieldOptions[`function:${operation}`] = {
  158. label: `${operation}(${'\u2026'})`,
  159. value: {
  160. kind: FieldValueKind.FUNCTION,
  161. meta: {
  162. name: operation,
  163. parameters: [
  164. {
  165. kind: 'column',
  166. columnTypes: [...typesByOp[operation]],
  167. defaultValue: fields[0].mri,
  168. required: true,
  169. },
  170. ],
  171. },
  172. },
  173. };
  174. });
  175. metaReponse
  176. .sort((a, b) => a.name.localeCompare(b.name))
  177. .forEach(field => {
  178. fieldOptions[`field:${field.mri}`] = {
  179. label: field.name,
  180. value: {
  181. kind: FieldValueKind.METRICS,
  182. meta: {
  183. name: field.mri,
  184. dataType: field.type,
  185. },
  186. },
  187. };
  188. });
  189. return fieldOptions;
  190. });
  191. }
  192. function filterMetricOperations(option: FieldValueOption) {
  193. return option.value.kind === FieldValueKind.FUNCTION;
  194. }
  195. function filterMetricMRIs(option: FieldValueOption) {
  196. return option.value.kind === FieldValueKind.METRICS;
  197. }
  198. function getTagsForMetric(
  199. organization: Organization,
  200. _?: TagCollection,
  201. __?: CustomMeasurementCollection,
  202. api?: Client,
  203. queries?: WidgetQuery[]
  204. ) {
  205. const fieldOptions = {};
  206. if (!api) {
  207. return fieldOptions;
  208. }
  209. const field = queries?.[0].aggregates[0] ?? '';
  210. const mri = getMRI(field);
  211. const useCase = getUseCaseFromMRI(mri);
  212. return api
  213. .requestPromise(`/organizations/${organization.slug}/metrics/tags/`, {
  214. query: {metric: mri, useCase},
  215. })
  216. .then(tagsResponse => {
  217. tagsResponse.forEach(tag => {
  218. fieldOptions[`field:${tag.key}`] = {
  219. label: tag.key,
  220. value: {
  221. kind: FieldValueKind.TAG,
  222. meta: {name: tag.key, dataType: 'string'},
  223. },
  224. };
  225. });
  226. return fieldOptions;
  227. });
  228. }
  229. function getMetricSeriesRequest(
  230. api: Client,
  231. widget: Widget,
  232. queryIndex: number,
  233. organization: Organization,
  234. pageFilters: PageFilters
  235. ) {
  236. const query = widget.queries[queryIndex];
  237. return getMetricRequest(api, query, organization, pageFilters, widget.limit);
  238. }
  239. function handleMetricTableOrderByReset(widgetQuery: WidgetQuery, newFields: string[]) {
  240. const disableSortBy = widgetQuery.columns.includes('session.status');
  241. if (disableSortBy) {
  242. widgetQuery.orderby = '';
  243. }
  244. return handleOrderByReset(widgetQuery, newFields);
  245. }
  246. export function transformMetricsResponseToTable(data: MetricsApiResponse): TableData {
  247. const rows = data.groups.map((group, index) => {
  248. const groupColumn = mapMetricGroupsToFields(group.by);
  249. const value = mapMetricGroupsToFields(group.totals);
  250. return {
  251. id: String(index),
  252. ...groupColumn,
  253. ...value,
  254. };
  255. });
  256. const singleRow = rows[0];
  257. const meta = {
  258. ...changeObjectValuesToTypes(omit(singleRow, 'id')),
  259. };
  260. return {meta, data: rows};
  261. }
  262. function mapMetricGroupsToFields(
  263. results: Record<string, number | string | null> | undefined
  264. ) {
  265. if (!results) {
  266. return {};
  267. }
  268. const mappedResults: typeof results = {};
  269. for (const [key, value] of Object.entries(results)) {
  270. mappedResults[key] = value;
  271. }
  272. return mappedResults;
  273. }
  274. function changeObjectValuesToTypes(
  275. obj: Record<string, number | string | null> | undefined
  276. ) {
  277. return Object.entries(obj ?? {}).reduce((acc, [key, value]) => {
  278. acc[key] = typeof value;
  279. return acc;
  280. }, {});
  281. }
  282. export function transformMetricsResponseToSeries(
  283. data: MetricsApiResponse,
  284. widgetQuery: WidgetQuery
  285. ) {
  286. if (data === null) {
  287. return [];
  288. }
  289. const results: Series[] = [];
  290. const queryAlias = widgetQuery.name;
  291. if (!data.groups.length) {
  292. return [
  293. {
  294. seriesName: `(${t('no results')})`,
  295. data: data.intervals.map(interval => ({
  296. name: interval,
  297. value: 0,
  298. })),
  299. },
  300. ];
  301. }
  302. data.groups.forEach(group => {
  303. Object.keys(group.series).forEach(field => {
  304. results.push({
  305. seriesName:
  306. queryAlias ||
  307. getSeriesName(group, data.groups.length === 1, widgetQuery.columns),
  308. data: data.intervals.map((interval, index) => ({
  309. name: interval,
  310. value: group.series[field][index] ?? 0,
  311. })),
  312. });
  313. });
  314. });
  315. return results.sort((a, b) => {
  316. return a.data[0].value < b.data[0].value ? -1 : 1;
  317. });
  318. }
  319. function getMetricRequest(
  320. api: Client,
  321. query: WidgetQuery,
  322. organization: Organization,
  323. pageFilters: PageFilters,
  324. limit?: number
  325. ): Promise<[MetricsApiResponse, string | undefined, ResponseMeta | undefined]> {
  326. if (!query.aggregates[0]) {
  327. // No aggregate selected, return empty response
  328. return Promise.resolve([
  329. {
  330. intervals: [],
  331. groups: [],
  332. meta: [],
  333. },
  334. 'OK',
  335. {
  336. getResponseHeader: () => '',
  337. },
  338. ] as any);
  339. }
  340. const per_page = limit && Number(limit) >= 10 ? limit : 10;
  341. const requestData = getMetricsApiRequestQuery(
  342. {
  343. field: query.aggregates[0],
  344. query: query.conditions,
  345. groupBy: query.columns,
  346. },
  347. pageFilters,
  348. {
  349. per_page,
  350. useNewMetricsLayer: false,
  351. }
  352. );
  353. const pathname = `/organizations/${organization.slug}/metrics/data/`;
  354. return api.requestPromise(pathname, {
  355. includeAllArgs: true,
  356. query: requestData,
  357. });
  358. }