metrics.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  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(
  238. api,
  239. query,
  240. organization,
  241. pageFilters,
  242. widget.limit,
  243. widget.displayType
  244. );
  245. }
  246. function handleMetricTableOrderByReset(widgetQuery: WidgetQuery, newFields: string[]) {
  247. const disableSortBy = widgetQuery.columns.includes('session.status');
  248. if (disableSortBy) {
  249. widgetQuery.orderby = '';
  250. }
  251. return handleOrderByReset(widgetQuery, newFields);
  252. }
  253. export function transformMetricsResponseToTable(data: MetricsApiResponse): TableData {
  254. const rows = data.groups.map((group, index) => {
  255. const groupColumn = mapMetricGroupsToFields(group.by);
  256. const value = mapMetricGroupsToFields(group.totals);
  257. return {
  258. id: String(index),
  259. ...groupColumn,
  260. ...value,
  261. };
  262. });
  263. const singleRow = rows[0];
  264. const meta = {
  265. ...changeObjectValuesToTypes(omit(singleRow, 'id')),
  266. };
  267. return {meta, data: rows};
  268. }
  269. function mapMetricGroupsToFields(
  270. results: Record<string, number | string | null> | undefined
  271. ) {
  272. if (!results) {
  273. return {};
  274. }
  275. const mappedResults: typeof results = {};
  276. for (const [key, value] of Object.entries(results)) {
  277. mappedResults[key] = value;
  278. }
  279. return mappedResults;
  280. }
  281. function changeObjectValuesToTypes(
  282. obj: Record<string, number | string | null> | undefined
  283. ) {
  284. return Object.entries(obj ?? {}).reduce((acc, [key, value]) => {
  285. acc[key] = typeof value;
  286. return acc;
  287. }, {});
  288. }
  289. export function transformMetricsResponseToSeries(
  290. data: MetricsApiResponse,
  291. widgetQuery: WidgetQuery
  292. ) {
  293. if (data === null) {
  294. return [];
  295. }
  296. const results: Series[] = [];
  297. const queryAlias = widgetQuery.name;
  298. if (!data.groups.length) {
  299. return [
  300. {
  301. seriesName: `(${t('no results')})`,
  302. data: data.intervals.map(interval => ({
  303. name: interval,
  304. value: 0,
  305. })),
  306. },
  307. ];
  308. }
  309. data.groups.forEach(group => {
  310. Object.keys(group.series).forEach(field => {
  311. results.push({
  312. seriesName:
  313. queryAlias ||
  314. getSeriesName(group, data.groups.length === 1, widgetQuery.columns),
  315. data: data.intervals.map((interval, index) => ({
  316. name: interval,
  317. value: group.series[field][index] ?? 0,
  318. })),
  319. });
  320. });
  321. });
  322. return results.sort((a, b) => {
  323. return a.data[0].value < b.data[0].value ? -1 : 1;
  324. });
  325. }
  326. function getMetricRequest(
  327. api: Client,
  328. query: WidgetQuery,
  329. organization: Organization,
  330. pageFilters: PageFilters,
  331. limit?: number,
  332. displayType?: DisplayType
  333. ): Promise<[MetricsApiResponse, string | undefined, ResponseMeta | undefined]> {
  334. if (!query.aggregates[0]) {
  335. // No aggregate selected, return empty response
  336. return Promise.resolve([
  337. {
  338. intervals: [],
  339. groups: [],
  340. meta: [],
  341. },
  342. 'OK',
  343. {
  344. getResponseHeader: () => '',
  345. },
  346. ] as any);
  347. }
  348. const per_page = limit && Number(limit) >= 10 ? limit : 10;
  349. const useNewMetricsLayer = organization.features.includes(
  350. 'metrics-api-new-metrics-layer'
  351. );
  352. const requestData = getMetricsApiRequestQuery(
  353. {
  354. field: query.aggregates[0],
  355. query: query.conditions,
  356. groupBy: query.columns,
  357. },
  358. pageFilters,
  359. {
  360. per_page,
  361. useNewMetricsLayer,
  362. fidelity: displayType === DisplayType.BAR ? 'low' : 'high',
  363. }
  364. );
  365. const pathname = `/organizations/${organization.slug}/metrics/data/`;
  366. return api.requestPromise(pathname, {
  367. includeAllArgs: true,
  368. query: requestData,
  369. });
  370. }