metrics.tsx 11 KB

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