metrics.tsx 11 KB

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