diff --git a/app-config.yaml b/app-config.yaml index d1414582ed..966f067ff5 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -236,11 +236,9 @@ costInsights: name: Big Query icon: search metrics: - dailyCost: - name: Pied Piper's Daily Cost - compare: DAU DAU: name: Daily Active Users + default: true MSC: name: Monthly Subscribers homepage: diff --git a/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts b/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts index fc2249fadc..0b275baf74 100644 --- a/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts +++ b/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts @@ -117,10 +117,7 @@ export class ExampleCostInsightsClient implements CostInsightsApi { return projects; } - async getMetricData( - metric: string | null, - intervals: string, - ): Promise { + async getMetricData(metric: string, intervals: string): Promise { const aggregation = aggregationFor( durationOf(intervals), 100_000, diff --git a/plugins/cost-insights/README.md b/plugins/cost-insights/README.md index 26f9d9371a..3028d3a09c 100644 --- a/plugins/cost-insights/README.md +++ b/plugins/cost-insights/README.md @@ -79,9 +79,9 @@ costInsights: ### Metrics (Optional) -In the `Cost Overview` panel, users can choose from a dropdown of business metrics to see costs as they relate to a metric, such as daily active users. Metrics must be defined as keys on the `metrics` field. A user-friendly name is **required**. Metrics will be provided to the `getDailyCost` and `getProjectCosts` API methods via the `metric` parameter. +In the `Cost Overview` panel, users can choose from a dropdown of business metrics to see costs as they relate to a metric, such as daily active users. Metrics must be defined as keys on the `metrics` field. A user-friendly name is **required**. Metrics will be provided to the `getMetricData` API method via the `metric` parameter. -**Note:** Cost Insights displays daily cost without a metric by default. The dropdown text for this default can be overridden by assigning it a value on the `dailyCost` field. +An optional `default` field can be set to `true` to set the default comparison metric to daily cost in the Cost Overview panel. ```yaml ## ./app-config.yaml @@ -95,10 +95,9 @@ costInsights: name: Some Other Cloud Product icon: data metrics: - dailyCost: - name: Earth Rotation metricA: name: Metric A ## required + default: true metricB: name: Metric B metricC: diff --git a/plugins/cost-insights/src/api/CostInsightsApi.ts b/plugins/cost-insights/src/api/CostInsightsApi.ts index 52b3f335d9..3136fb4c27 100644 --- a/plugins/cost-insights/src/api/CostInsightsApi.ts +++ b/plugins/cost-insights/src/api/CostInsightsApi.ts @@ -87,7 +87,7 @@ export type CostInsightsApi = { * @param intervals An ISO 8601 repeating interval string, such as R2/P1M/2020-09-01 * https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals */ - getMetricData(metric: string | null, intervals: string): Promise; + getMetricData(metric: string, intervals: string): Promise; /** * Get cost aggregations for a particular cloud product and interval timeframe. This includes @@ -104,10 +104,9 @@ export type CostInsightsApi = { * @param product The product from the cost-insights configuration in app-config.yaml * @param group * @param duration A time duration, such as P1M. See the Duration type for a detailed explanation - * of how the durations are interpreted in Cost Insights. + * of how the durations are interpreted in Cost Insights. * @param project (optional) The project id from getGroupProjects or query parameters */ - getProductInsights( product: string, group: string, diff --git a/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx b/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx index 3ec2828029..7ca4958c8e 100644 --- a/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx +++ b/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx @@ -40,6 +40,7 @@ const mockMetrics: Metric[] = [ { kind: 'some-metric', name: 'Some Metric', + default: false, }, ]; diff --git a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx index 9bc396537b..20f965f1c7 100644 --- a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx +++ b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx @@ -101,34 +101,26 @@ const CostInsightsPage = () => { try { if (pageFilters.group) { dispatchLoadingInsights(true); + const intervals = intervalsOf(pageFilters.duration); const [ fetchedProjects, fetchedAlerts, fetchedMetricData, - fetchedCosts, + fetchedDailyCost, ] = await Promise.all([ client.getGroupProjects(pageFilters.group), client.getAlerts(pageFilters.group), pageFilters.metric - ? client.getMetricData( - pageFilters.metric, - intervalsOf(pageFilters.duration), - ) + ? client.getMetricData(pageFilters.metric, intervals) : null, pageFilters.project - ? client.getProjectDailyCost( - pageFilters.project, - intervalsOf(pageFilters.duration), - ) - : client.getGroupDailyCost( - pageFilters.group, - intervalsOf(pageFilters.duration), - ), + ? client.getProjectDailyCost(pageFilters.project, intervals) + : client.getGroupDailyCost(pageFilters.group, intervals), ]); setProjects(fetchedProjects); setAlerts(fetchedAlerts); setMetricData(fetchedMetricData); - setDailyCost(fetchedCosts); + setDailyCost(fetchedDailyCost); } else { dispatchLoadingNone(loadingActions); } @@ -267,7 +259,10 @@ const CostInsightsPage = () => { {!!dailyCost.aggregation.length && ( - + )} diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx index daffe335c5..a08e7fc789 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx @@ -35,10 +35,14 @@ import { } from '../../types'; export type CostOverviewCardProps = { - data: [Cost, MetricData | null]; + dailyCostData: Cost; + metricData: MetricData | null; }; -const CostOverviewCard = ({ data }: CostOverviewCardProps) => { +const CostOverviewCard = ({ + dailyCostData, + metricData, +}: CostOverviewCardProps) => { const theme = useTheme(); const config = useConfig(); const { ScrollAnchor } = useScroll(DefaultNavigation.CostOverviewCard); @@ -46,10 +50,12 @@ const CostOverviewCard = ({ data }: CostOverviewCardProps) => { mapFiltersToProps, ); - // There should always be a daily cost metric but a comparison metric is optional. - const dailyCost = findAlways(config.metrics, m => m.kind === null); - const metric = config.metrics.find(m => m.kind === filters.metric); - const comparedChange = data[1] ? getComparedChange(data[0], data[1]) : null; + const metric = filters.metric + ? findAlways(config.metrics, m => m.kind === filters.metric) + : null; + const comparedChange = metricData + ? getComparedChange(dailyCostData, metricData) + : null; return ( @@ -62,21 +68,18 @@ const CostOverviewCard = ({ data }: CostOverviewCardProps) => { - - {formatPercent(data[0].change.ratio)} + + {formatPercent(dailyCostData.change.ratio)} - {metric && metric.kind && data[1] && comparedChange && ( + {metric && metricData && comparedChange && ( <> - {formatPercent(data[1].change.ratio)} + {formatPercent(metricData.change.ratio)} { )} diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx index 40cefbb589..9612589542 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx @@ -16,7 +16,6 @@ import React from 'react'; import { useTheme } from '@material-ui/core'; import { - AxisDomain, ComposedChart, XAxis, YAxis, @@ -30,6 +29,7 @@ import { import { ChartData, Cost, + Maybe, Metric, MetricData, CostInsightsTheme, @@ -40,71 +40,69 @@ import { } from '../../utils/graphs'; import CostOverviewTooltip from './CostOverviewTooltip'; import { TooltipItemProps } from '../Tooltip'; -import { NULL_METRIC } from '../../hooks/useConfig'; import { useCostOverviewStyles as useStyles } from '../../utils/styles'; import { groupByDate, toDataMax, trendFrom } from '../../utils/charts'; import { aggregationSort } from '../../utils/sort'; type CostOverviewChartProps = { - data: [Cost, MetricData | null]; - name: string; - compare?: Metric; + metric: Maybe; + metricData: Maybe; + dailyCostData: Cost; responsive?: boolean; }; const CostOverviewChart = ({ - data, - name, - compare, + dailyCostData, + metric, + metricData, responsive = true, }: CostOverviewChartProps) => { const theme = useTheme(); const styles = useStyles(theme); - const { dailyCost, metric } = { + const data = { dailyCost: { - id: NULL_METRIC, - name: name, - format: 'number', - data: data[0], + dataKey: 'dailyCost', + name: `Daily Cost`, + format: 'currency', + data: dailyCostData, }, metric: { - id: compare?.kind ?? 'Unknown', - name: compare?.name ?? 'Unknown', - format: data[1]?.format ?? 'number', - data: data[1], + dataKey: metric?.kind ?? 'Unknown', + name: metric?.name ?? 'Unknown', + format: metricData?.format ?? 'number', + data: metricData, }, }; - const metricsByDate = metric.data - ? metric.data.aggregation.reduce(groupByDate, {}) + const metricsByDate = data.metric.data + ? data.metric.data.aggregation.reduce(groupByDate, {}) : {}; - const chartData: ChartData[] = dailyCost - .data!.aggregation.slice() + const chartData: ChartData[] = data.dailyCost.data.aggregation + .slice() .sort(aggregationSort) .map(entry => ({ date: Date.parse(entry.date), - trend: trendFrom(dailyCost.data!.trendline, Date.parse(entry.date)), + trend: trendFrom(data.dailyCost.data.trendline, Date.parse(entry.date)), dailyCost: entry.amount, - ...(metric && metric.data - ? { [metric.id]: metricsByDate[`${entry.date}`] } + ...(metric && data.metric.data + ? { [data.metric.dataKey]: metricsByDate[`${entry.date}`] } : {}), })); - const metricDataMax: AxisDomain = metric - ? toDataMax(metric.id, chartData) - : 'dataMax'; - function tooltipFormatter(payload: TooltipPayload): TooltipItemProps { return { - label: payload.dataKey === dailyCost.id ? dailyCost.name : metric.name, + label: + payload.dataKey === data.dailyCost.dataKey + ? data.dailyCost.name + : data.metric.name, value: - payload.dataKey === dailyCost.id - ? formatGraphValue(payload.value as number, dailyCost.format) - : formatGraphValue(payload.value as number, metric.format), + payload.dataKey === data.dailyCost.dataKey + ? formatGraphValue(payload.value as number, data.dailyCost.format) + : formatGraphValue(payload.value as number, data.metric.format), fill: - payload.dataKey === dailyCost.id + payload.dataKey === data.dailyCost.dataKey ? theme.palette.blue : theme.palette.magenta, }; @@ -131,23 +129,23 @@ const CostOverviewChart = ({ tick={{ fill: styles.axis.fill }} tickFormatter={formatGraphValue} width={styles.yAxis.width} - yAxisId={dailyCost.id} + yAxisId={data.dailyCost.dataKey} /> {metric && ( 0, metricDataMax]} + domain={[() => 0, toDataMax(data.metric.dataKey, chartData)]} width={styles.yAxis.width} - yAxisId={metric.id} + yAxisId={data.metric.dataKey} /> )} {metric && ( )} } diff --git a/plugins/cost-insights/src/components/MetricSelect/MetricSelect.test.tsx b/plugins/cost-insights/src/components/MetricSelect/MetricSelect.test.tsx index 67d1ec3ad3..e78fd77b1a 100644 --- a/plugins/cost-insights/src/components/MetricSelect/MetricSelect.test.tsx +++ b/plugins/cost-insights/src/components/MetricSelect/MetricSelect.test.tsx @@ -23,7 +23,7 @@ describe('', () => { it('should display a metric', async () => { const mockProps: MetricSelectProps = { metric: 'test', - metrics: [{ kind: 'test', name: 'some-name' }], + metrics: [{ kind: 'test', name: 'some-name', default: false }], onSelect: jest.fn(), }; const { getByText } = await renderInTestApp( @@ -32,25 +32,12 @@ describe('', () => { expect(getByText(/some-name/)).toBeInTheDocument(); }); - it('should display a null metric', async () => { - const mockProps: MetricSelectProps = { - metric: null, - metrics: [{ kind: null, name: 'billie-nullish' }], - onSelect: jest.fn(), - }; - const { getByText } = await renderInTestApp( - , - ); - expect(getByText(/billie-nullish/)).toBeInTheDocument(); - }); - it('should display all metrics', async () => { const mockProps: MetricSelectProps = { metric: null, metrics: [ - { kind: null, name: 'billie-nullish' }, - { kind: 'MAU1M', name: 'Cost Per Million MAU' }, - { kind: 'my-cool-metric', name: 'metric-mcmetric-face' }, + { kind: 'DAU', name: 'Daily Active Users', default: true }, + { kind: 'MSC', name: 'Monthly Subscribers', default: false }, ], onSelect: jest.fn(), }; @@ -61,11 +48,10 @@ describe('', () => { UserEvent.click(button); - await waitFor(() => getAllByText(/billie-nullish/)); + await waitFor(() => getAllByText(/None/)); // The active metric should display in the popver list and in the input - expect(getAllByText(/billie-nullish/).length).toBe(2); - expect(getByText(/Cost Per Million MAU/)).toBeInTheDocument(); - expect(getByText(/metric-mcmetric-face/)).toBeInTheDocument(); + expect(getByText(/Daily Active Users/)).toBeInTheDocument(); + expect(getByText(/Monthly Subscribers/)).toBeInTheDocument(); }); }); diff --git a/plugins/cost-insights/src/components/MetricSelect/MetricSelect.tsx b/plugins/cost-insights/src/components/MetricSelect/MetricSelect.tsx index 1d3af911d4..474d8622ea 100644 --- a/plugins/cost-insights/src/components/MetricSelect/MetricSelect.tsx +++ b/plugins/cost-insights/src/components/MetricSelect/MetricSelect.tsx @@ -15,10 +15,9 @@ */ import React from 'react'; -import { Select, MenuItem } from '@material-ui/core'; -import { Maybe, Metric, findAlways } from '../../types'; +import { InputLabel, FormControl, Select, MenuItem } from '@material-ui/core'; +import { Maybe, Metric } from '../../types'; import { useSelectStyles as useStyles } from '../../utils/styles'; -import { NULL_METRIC } from '../../hooks/useConfig'; export type MetricSelectProps = { metric: Maybe; @@ -29,38 +28,37 @@ export type MetricSelectProps = { const MetricSelect = ({ metric, metrics, onSelect }: MetricSelectProps) => { const classes = useStyles(); - const handleOnChange = (e: React.ChangeEvent<{ value: unknown }>) => { - if (e.target.value === NULL_METRIC) { + function onChange(e: React.ChangeEvent<{ value: unknown }>) { + if (e.target.value === 'none') { onSelect(null); } else { onSelect(e.target.value as string); } - }; - - const renderValue = (value: unknown) => { - const kind = (value === NULL_METRIC ? null : value) as Maybe; - const { name } = findAlways(metrics, m => m.kind === kind); - return {name}; - }; + } return ( - + + None - ))} - + {metrics.map((m: Metric) => ( + + {m.name} + + ))} + + ); }; diff --git a/plugins/cost-insights/src/hooks/useConfig.tsx b/plugins/cost-insights/src/hooks/useConfig.tsx index 830cafc4f1..6cbdd1a2ef 100644 --- a/plugins/cost-insights/src/hooks/useConfig.tsx +++ b/plugins/cost-insights/src/hooks/useConfig.tsx @@ -25,12 +25,10 @@ import { useApi, configApiRef } from '@backstage/core'; import { Config as BackstageConfig } from '@backstage/config'; import { Currency, defaultCurrencies, Product, Icon, Metric } from '../types'; import { getIcon } from '../utils/navigation'; - -export const NULL_METRIC = 'dailyCost'; -export const NULL_METRIC_NAME = 'Daily Cost'; +import { validateMetrics } from '../utils/config'; /* - * Config schema 2020-09-28 + * Config schema 2020-10-15 * * costInsights: * engineerCost: 200000 @@ -42,11 +40,9 @@ export const NULL_METRIC_NAME = 'Daily Cost'; * name: Product B * icon: data * metrics: - * dailyCost: - * name: Daily Cost - * compare: metricA * metricA: * name: Metric A + * default: true * metricB: * name: Metric B */ @@ -64,7 +60,7 @@ export const ConfigContext = createContext( ); const defaultState: ConfigContextProps = { - metrics: [{ kind: null, name: NULL_METRIC_NAME }], + metrics: [], products: [], icons: [], engineerCost: 0, @@ -90,9 +86,9 @@ export const ConfigProvider = ({ children }: { children: ReactNode }) => { const metrics = c.getOptionalConfig('costInsights.metrics'); if (metrics) { return metrics.keys().map(key => ({ - kind: key === NULL_METRIC ? null : key, + kind: key, name: metrics.getString(`${key}.name`), - compare: metrics.getOptionalString(`${key}.compare`), + default: metrics.getOptionalBoolean(`${key}.default`) ?? false, })); } @@ -119,23 +115,15 @@ export const ConfigProvider = ({ children }: { children: ReactNode }) => { const engineerCost = getEngineerCost(); const icons = getIcons(); - if (metrics.find((m: Metric) => m.kind === null)) { - setConfig(prevState => ({ - ...prevState, - metrics, - products, - engineerCost, - icons, - })); - } else { - setConfig(prevState => ({ - ...prevState, - metrics: [...prevState.metrics, ...metrics], - products, - engineerCost, - icons, - })); - } + validateMetrics(metrics); + + setConfig(prevState => ({ + ...prevState, + metrics, + products, + engineerCost, + icons, + })); setLoading(false); } @@ -154,12 +142,7 @@ export const ConfigProvider = ({ children }: { children: ReactNode }) => { export function useConfig(): ConfigContextProps { const config = useContext(ConfigContext); - - if (!config) { - assertNever(); - } - - return config; + return config ? config : assertNever(); } function assertNever(): never { diff --git a/plugins/cost-insights/src/hooks/useFilters.tsx b/plugins/cost-insights/src/hooks/useFilters.tsx index 0ad4c24267..7dcf8ddb26 100644 --- a/plugins/cost-insights/src/hooks/useFilters.tsx +++ b/plugins/cost-insights/src/hooks/useFilters.tsx @@ -75,9 +75,8 @@ export const FilterProvider = ({ children }: FilterProviderProps) => { const queryParams = useQueryParams(); const qsRef = useRef(''); const groups = useGroups(); - const { products } = useConfig(); - const defaultProductFilters = products.map(product => ({ + const defaultProductFilters = config.products.map(product => ({ productType: product.kind, duration: Duration.P1M, })); @@ -103,8 +102,8 @@ export const FilterProvider = ({ children }: FilterProviderProps) => { // TODO: Figure out why pageFilters doesn't get updated by the above when groups are loaded. useEffect(() => { const initialState = getInitialPageState(groups, queryParams.pageFilters); - const compared = config.metrics.find(m => m.kind === null)?.compare; - setPageFilters({ ...initialState, metric: compared || null }); + const defaultMetric = config.metrics.find(m => m.default); + setPageFilters({ ...initialState, metric: defaultMetric?.kind ?? null }); }, [groups]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { diff --git a/plugins/cost-insights/src/types/ChangeStatistic.ts b/plugins/cost-insights/src/types/ChangeStatistic.ts index cf7310ef97..cdd2e02da6 100644 --- a/plugins/cost-insights/src/types/ChangeStatistic.ts +++ b/plugins/cost-insights/src/types/ChangeStatistic.ts @@ -15,6 +15,7 @@ */ import { Cost } from './Cost'; +import { MetricData } from './MetricData'; import { aggregationSort } from '../utils/sort'; export interface ChangeStatistic { @@ -51,9 +52,12 @@ export function growthOf(amount: number, ratio: number) { } // Used by for displaying engineer totals -export function getComparedChange(a: Cost, b: Cost): ChangeStatistic { - const ratio = a.change.ratio - b.change.ratio; - const amount = a.aggregation.slice().sort(aggregationSort)[0].amount; +export function getComparedChange( + dailyCost: Cost, + metricData: MetricData, +): ChangeStatistic { + const ratio = dailyCost.change.ratio - metricData.change.ratio; + const amount = dailyCost.aggregation.slice().sort(aggregationSort)[0].amount; return { ratio: ratio, amount: amount * ratio, diff --git a/plugins/cost-insights/src/types/Cost.ts b/plugins/cost-insights/src/types/Cost.ts index 8a9ce3c69b..84a8bfae51 100644 --- a/plugins/cost-insights/src/types/Cost.ts +++ b/plugins/cost-insights/src/types/Cost.ts @@ -24,7 +24,3 @@ export interface Cost { change: ChangeStatistic; trendline: Trendline; } - -export interface MetricData extends Cost { - format: 'number' | 'currency'; -} diff --git a/plugins/cost-insights/src/types/Metric.ts b/plugins/cost-insights/src/types/Metric.ts index 4fa9cd189b..b0a0c9cc76 100644 --- a/plugins/cost-insights/src/types/Metric.ts +++ b/plugins/cost-insights/src/types/Metric.ts @@ -14,10 +14,8 @@ * limitations under the License. */ -import { Maybe } from '../types'; - export type Metric = { - kind: Maybe; + kind: string; name: string; - compare?: string; + default: boolean; }; diff --git a/plugins/cost-insights/src/types/MetricData.ts b/plugins/cost-insights/src/types/MetricData.ts new file mode 100644 index 0000000000..bd3bdde735 --- /dev/null +++ b/plugins/cost-insights/src/types/MetricData.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DateAggregation } from './DateAggregation'; +import { ChangeStatistic } from './ChangeStatistic'; + +export interface MetricData { + id: string; + format: 'number' | 'currency'; + aggregation: DateAggregation[]; + change: ChangeStatistic; +} diff --git a/plugins/cost-insights/src/types/index.ts b/plugins/cost-insights/src/types/index.ts index 8523c7db7b..398110ec80 100644 --- a/plugins/cost-insights/src/types/index.ts +++ b/plugins/cost-insights/src/types/index.ts @@ -27,6 +27,7 @@ export * from './Filters'; export * from './Group'; export * from './Loading'; export * from './Maybe'; +export * from './MetricData'; export * from './Metric'; export * from './Product'; export * from './Project'; diff --git a/plugins/cost-insights/src/utils/config.ts b/plugins/cost-insights/src/utils/config.ts new file mode 100644 index 0000000000..c2c2c066d9 --- /dev/null +++ b/plugins/cost-insights/src/utils/config.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Metric } from '../types'; + +export function validateMetrics(metrics: Metric[]) { + let defaultMetric = null; + for (const metric of metrics) { + if (metric.default) { + if (defaultMetric) { + throw new Error( + `Cannot set default for multiple metrics: Received: ${defaultMetric.kind} and ${metric.kind}`, + ); + } else { + defaultMetric = metric; + } + } + } +}