metrics.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import omit from 'lodash/omit';
  2. import {Client, ResponseMeta} from 'sentry/api';
  3. import {t} from 'sentry/locale';
  4. import {MetricsApiResponse, Organization, PageFilters} from 'sentry/types';
  5. import {Series} from 'sentry/types/echarts';
  6. import {TableData} from 'sentry/utils/discover/discoverQuery';
  7. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  8. import {getMetricsApiRequestQuery, getSeriesName} from 'sentry/utils/metrics';
  9. import {OnDemandControlContext} from 'sentry/utils/performance/contexts/onDemandControl';
  10. import {MetricSearchBar} from 'sentry/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/metricSearchBar';
  11. import {DisplayType, Widget, WidgetQuery} from '../types';
  12. import {DatasetConfig, handleOrderByReset} from './base';
  13. const DEFAULT_WIDGET_QUERY: WidgetQuery = {
  14. name: '',
  15. fields: [`avg(duration)`],
  16. columns: [],
  17. fieldAliases: [],
  18. aggregates: [`avg(duration)`],
  19. conditions: '',
  20. orderby: `-avg(duration)`,
  21. };
  22. export const MetricsConfig: DatasetConfig<MetricsApiResponse, MetricsApiResponse> = {
  23. defaultWidgetQuery: DEFAULT_WIDGET_QUERY,
  24. enableEquations: false,
  25. getTableRequest: (
  26. api: Client,
  27. _: Widget,
  28. query: WidgetQuery,
  29. organization: Organization,
  30. pageFilters: PageFilters,
  31. __?: OnDemandControlContext,
  32. limit?: number
  33. ) => getMetricRequest(api, query, organization, pageFilters, limit),
  34. getSeriesRequest: getMetricSeriesRequest,
  35. getCustomFieldRenderer: (field, meta) => getFieldRenderer(field, meta, false),
  36. SearchBar: MetricSearchBar,
  37. handleOrderByReset: handleMetricTableOrderByReset,
  38. supportedDisplayTypes: [
  39. DisplayType.AREA,
  40. DisplayType.BAR,
  41. DisplayType.BIG_NUMBER,
  42. DisplayType.LINE,
  43. DisplayType.TABLE,
  44. DisplayType.TOP_N,
  45. ],
  46. transformSeries: transformMetricsResponseToSeries,
  47. transformTable: transformMetricsResponseToTable,
  48. getTableFieldOptions: () => ({}),
  49. getTimeseriesSortOptions: () => ({}),
  50. getTableSortOptions: undefined,
  51. };
  52. function getMetricSeriesRequest(
  53. api: Client,
  54. widget: Widget,
  55. queryIndex: number,
  56. organization: Organization,
  57. pageFilters: PageFilters
  58. ) {
  59. const query = widget.queries[queryIndex];
  60. return getMetricRequest(api, query, organization, pageFilters, widget.limit);
  61. }
  62. function handleMetricTableOrderByReset(widgetQuery: WidgetQuery, newFields: string[]) {
  63. const disableSortBy = widgetQuery.columns.includes('session.status');
  64. if (disableSortBy) {
  65. widgetQuery.orderby = '';
  66. }
  67. return handleOrderByReset(widgetQuery, newFields);
  68. }
  69. export function transformMetricsResponseToTable(
  70. data: MetricsApiResponse,
  71. {aggregates}: WidgetQuery
  72. ): TableData {
  73. // TODO(ddm): get rid of this mapping, it is only needed because the API returns
  74. // `op(metric_name)` instead of `op(mri)`
  75. const rows = mapResponse(data, aggregates).groups.map((group, index) => {
  76. const groupColumn = mapDerivedMetricsToFields(group.by);
  77. const value = mapDerivedMetricsToFields(group.totals);
  78. return {
  79. id: String(index),
  80. ...groupColumn,
  81. ...value,
  82. };
  83. });
  84. const singleRow = rows[0];
  85. const meta = {
  86. ...changeObjectValuesToTypes(omit(singleRow, 'id')),
  87. };
  88. return {meta, data: rows};
  89. }
  90. function mapDerivedMetricsToFields(
  91. results: Record<string, number | string | null> | undefined,
  92. mapToKey?: string
  93. ) {
  94. if (!results) {
  95. return {};
  96. }
  97. const mappedResults: typeof results = {};
  98. for (const [key, value] of Object.entries(results)) {
  99. mappedResults[mapToKey ?? key] = value;
  100. }
  101. return mappedResults;
  102. }
  103. function changeObjectValuesToTypes(
  104. obj: Record<string, number | string | null> | undefined
  105. ) {
  106. return Object.keys(obj ?? {}).reduce((acc, key) => {
  107. acc[key] = key.includes('@') ? 'number' : 'string';
  108. return acc;
  109. }, {});
  110. }
  111. export function transformMetricsResponseToSeries(
  112. data: MetricsApiResponse,
  113. widgetQuery: WidgetQuery
  114. ) {
  115. if (data === null) {
  116. return [];
  117. }
  118. const results: Series[] = [];
  119. if (!data.groups.length) {
  120. return [
  121. {
  122. seriesName: `(${t('no results')})`,
  123. data: data.intervals.map(interval => ({
  124. name: interval,
  125. value: 0,
  126. })),
  127. },
  128. ];
  129. }
  130. data.groups.forEach(group => {
  131. Object.keys(group.series).forEach(field => {
  132. results.push({
  133. seriesName: getSeriesName(group, data.groups.length === 1, widgetQuery.columns),
  134. data: data.intervals.map((interval, index) => ({
  135. name: interval,
  136. value: group.series[field][index] ?? 0,
  137. })),
  138. });
  139. });
  140. });
  141. return results;
  142. }
  143. function getMetricRequest(
  144. api: Client,
  145. query: WidgetQuery,
  146. organization: Organization,
  147. pageFilters: PageFilters,
  148. limit?: number
  149. ) {
  150. const requestData = getMetricsApiRequestQuery(
  151. {
  152. field: query.aggregates[0],
  153. query: query.conditions,
  154. groupBy: query.columns,
  155. },
  156. pageFilters,
  157. {
  158. per_page: query.columns.length === 0 ? 1 : limit,
  159. useNewMetricsLayer: false,
  160. }
  161. );
  162. const pathname = `/organizations/${organization.slug}/metrics/data/`;
  163. return api.requestPromise(pathname, {
  164. includeAllArgs: true,
  165. query: requestData,
  166. }) as Promise<[MetricsApiResponse, string | undefined, ResponseMeta | undefined]>;
  167. }
  168. const mapResponse = (data: MetricsApiResponse, field: string[]): MetricsApiResponse => {
  169. const mappedGroups = data.groups.map(group => {
  170. return {
  171. ...group,
  172. by: group.by,
  173. series: swapKeys(group.series, field),
  174. totals: swapKeys(group.totals, field),
  175. };
  176. });
  177. return {...data, groups: mappedGroups};
  178. };
  179. const swapKeys = (obj: Record<string, unknown> | undefined, newKeys: string[]) => {
  180. if (!obj) {
  181. return {};
  182. }
  183. const keys = Object.keys(obj);
  184. const values = Object.values(obj);
  185. const newObj = {};
  186. keys.forEach((_, index) => {
  187. newObj[newKeys[index]] = values[index];
  188. });
  189. return newObj;
  190. };