metrics.tsx 11 KB

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