-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[APM]: Inferred types for aggregations #37360
Conversation
@@ -37,7 +36,10 @@ export async function getErrorGroups({ | |||
}) { | |||
const { start, end, uiFiltersES, client, config } = setup; | |||
|
|||
const params: SearchParams = { | |||
// sort buckets by last occurrence of error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TS was complaining about changing the shape of the object after creating it, so I moved it inline.
@@ -22,14 +22,20 @@ const getRelativeImpact = ( | |||
); | |||
|
|||
function getWithRelativeImpact(items: TransactionListAPIResponse) { | |||
const impacts = items.map(({ impact }) => impact); | |||
const impacts = items |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
values from metric aggregations can be null
too
@@ -52,6 +50,9 @@ const chartBase: ChartBase<HeapMemoryMetrics> = { | |||
}; | |||
|
|||
export async function getHeapMemoryChart(setup: Setup, serviceName: string) { | |||
const result = await fetch(setup, serviceName); | |||
return transformDataToMetricsChart<HeapMemoryMetrics>(result, chartBase); | |||
const result = (await fetch(setup, serviceName)) as MetricSearchResponse< |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we don't explicitly tell which keys from aggregations
are related to the chart, TS will create a union type of date_histogram and min/max buckets, and TS does not allow (AFAICT) maps over union types: microsoft/TypeScript#29011 (comment)
CpuSearchResponse, | ||
'aggregations' | ||
> & { | ||
aggregations: IndexAsString< |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sometimes TS needs a little help in realising object keys are strings (something to do with the keyof operator).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i can understand it...since numbers and symbols are valid object keys too, but in our case, it will always be strings :)
@@ -91,7 +74,8 @@ export async function anomalySeriesFetcher({ | |||
}; | |||
|
|||
try { | |||
return await client<void, Aggs>('search', params); | |||
const response = await client<void, typeof params>('search', params); | |||
return response; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's kind of weird to me that this is in a try/catch and silently fails when there is a HttpError. Anyone who knows why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i don't know why it's like this, but i agree it seems strange. I would imagine that it might be this way so that a chart is simply omitted from rendering when there is an error, rather than displaying a chart + error messaging. But it seems like this is incomplete UX.
lower: number; | ||
}; | ||
}; | ||
}[AggregationType & keyof AggregationOption[AggregationName]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AggregationOption:
{
name_of_agg: {
terms: {
...
}
}
}
That needs to be translated to:
{
name_of_agg: {
buckets: []
}
}
That's what this part is for. It picks whatever key from name_of_agg
that is an aggregation type, and uses that to get the response type from the parent object. Dirty, but works.
💔 Build Failed |
142e695
to
d0cf61f
Compare
💔 Build Failed |
d0cf61f
to
d1c1a77
Compare
💔 Build Failed |
d1c1a77
to
1d78e90
Compare
💔 Build Failed |
💔 Build Failed |
50c1548
to
a5d4f1d
Compare
💚 Build Succeeded |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really great stuff. takes out much of the boilerplate type definitions, and fixes inconsistencies with ES types. Just a few comments I left below.
@@ -91,7 +74,8 @@ export async function anomalySeriesFetcher({ | |||
}; | |||
|
|||
try { | |||
return await client<void, Aggs>('search', params); | |||
const response = await client<void, typeof params>('search', params); | |||
return response; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i don't know why it's like this, but i agree it seems strange. I would imagine that it might be this way so that a chart is simply omitted from rendering when there is an error, rather than displaying a chart + error messaging. But it seems like this is incomplete UX.
keyof typeof chartBase.series | ||
>; | ||
|
||
return transformDataToMetricsChart(result, chartBase as ChartBase); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These type assertions could be obscuring type errors in the value. For chartBase
, i like how we no longer have to define a special interface for chart-specific metrics. but it's now possible that a type mismatch for the rest of chartBase
makes it thru to transformDataToMetricsChart
via the type assertion. It might be safer for chartBase
to be typed when it's defined.
A possible solution for best of both worlds:
const chartBaseSeries = {
heapMemoryUsed: {
title: i18n.translate('xpack.apm.agentMetrics.java.heapMemorySeriesUsed', {
defaultMessage: 'Avg. used'
}),
color: theme.euiColorVis0
},
heapMemoryCommitted: {
title: i18n.translate(
'xpack.apm.agentMetrics.java.heapMemorySeriesCommitted',
{
defaultMessage: 'Avg. committed'
}
),
color: theme.euiColorVis1
},
heapMemoryMax: {
title: i18n.translate('xpack.apm.agentMetrics.java.heapMemorySeriesMax', {
defaultMessage: 'Avg. limit'
}),
color: theme.euiColorVis2
}
};
const chartBase: ChartBase = {
title: i18n.translate('xpack.apm.agentMetrics.java.heapMemoryChartTitle', {
defaultMessage: 'Heap Memory'
}),
key: 'heap_memory_area_chart',
type: 'area',
yUnit: 'bytes',
series: chartBaseSeries
};
export async function getHeapMemoryChart(setup: Setup, serviceName: string) {
const result = await fetch<keyof typeof chartBaseSeries>(setup, serviceName);
return transformDataToMetricsChart(result, chartBase);
}
with fetch
defined as type:
export async function fetch<ChartBaseSeriesKey extends string>(
setup: Setup,
serviceName: string
): Promise<MetricSearchResponse<ChartBaseSeriesKey>> {
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah definitely looks better! I tried to make this work but ran into some issues when casting the return value to the return type. I've landed on a solution where instead of having several fetcher
s and fetch
calls, we have a generic fetchAndTransformMetrics
call with some parameters. fetchAndTransformMetrics
then calls transformDataToMetricsChart
, and TypeScript then - for some reason - is able to match the types together. Bonus is that we can merge a bunch of largely overlapping fetcher
files into one file.
x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts
Outdated
Show resolved
Hide resolved
CpuSearchResponse, | ||
'aggregations' | ||
> & { | ||
aggregations: IndexAsString< |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i can understand it...since numbers and symbols are valid object keys too, but in our case, it will always be strings :)
const values = transactionGroups.map(({ impact }) => impact); | ||
const values = transactionGroups | ||
.map(({ impact }) => impact) | ||
.filter(value => value !== null) as number[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the type assertion as number[]
is not needed here since it's inferred correctly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
impact
has a union type of number
and null
, and TypeScript can't figure out that we filtered out all non-null values in the filter function before it. I think in Lodash 4.x you can use compact
to achieve the same effect, but the typings for 3.x are not complete enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -27,7 +34,7 @@ function getTransactionGroup( | |||
const averageResponseTime = bucket.avg.value; | |||
const transactionsPerMinute = bucket.doc_count / minutes; | |||
const impact = bucket.sum.value; | |||
const sample = bucket.sample.hits.hits[0]._source; | |||
const sample = bucket.sample.hits.hits[0]._source as Transaction; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this type assertion as Transaction
should not be needed here since it inferred from _source
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That works for the top-level hits
object, but this is from a top_hits
aggregation, so it needs a type hint. I'm not sure how to solve it without the type hint because it's so dynamic. Do you have any ideas?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in this case, i think you're right to use the type assertion
a5d4f1d
to
0181fe2
Compare
💚 Build Succeeded |
2f055e3
to
dc0da4b
Compare
💔 Build Failed |
retest |
💚 Build Succeeded |
Previously, aggregations returned by the ESClient were 'any' by default, and the return type had to be explicitly defined by the consumer to get any type safety. This leads to both type duplication and errors because of wrong assumptions. This change infers the aggregation return type from the parameters passed to ESClient.search.
dc0da4b
to
8ac37e6
Compare
💚 Build Succeeded |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great!
* [APM]: Inferred types for aggregations Previously, aggregations returned by the ESClient were 'any' by default, and the return type had to be explicitly defined by the consumer to get any type safety. This leads to both type duplication and errors because of wrong assumptions. This change infers the aggregation return type from the parameters passed to ESClient.search. * Fix idx error * Safeguard against querying against non-existing indices in functional tests * Improve metric typings * Automatically infer params from function arguments * Remove unnecessary type hints
* [APM]: Inferred types for aggregations Previously, aggregations returned by the ESClient were 'any' by default, and the return type had to be explicitly defined by the consumer to get any type safety. This leads to both type duplication and errors because of wrong assumptions. This change infers the aggregation return type from the parameters passed to ESClient.search. * Fix idx error * Safeguard against querying against non-existing indices in functional tests * Improve metric typings * Automatically infer params from function arguments * Remove unnecessary type hints
Previously, aggregations returned by the ESClient were 'any' by default, and the return type had to be explicitly defined by the consumer to get any type safety. This leads to both type duplication and errors because of wrong assumptions.
This change infers the aggregation return type from the parameters passed to ESClient.search.
e.g. from the following parameters:
it will infer the return type (approximately):
Here's a TypeScript REPL.