-
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] Transaction breakdown charts #39531
[APM] Transaction breakdown charts #39531
Conversation
26c86e1
to
9bef809
Compare
💔 Build Failed |
ca232ba
to
d27fe32
Compare
💔 Build Failed |
💚 Build Succeeded |
1fe4eaf
to
85985c0
Compare
85985c0
to
7b6608f
Compare
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've bailed on refactoring CustomPlot and its sibilings, I ran into some issues w/ react-vis not having typings and it generally just being too much work to get through the process on such short notice. Would definitely like to pick it up some time soon, feel like we can gain a lot by improving our coverage there.
@@ -53,8 +54,9 @@ class StaticPlot extends PureComponent { | |||
curve={'curveMonotoneX'} | |||
data={serie.data} | |||
color={serie.color} | |||
stroke={serie.color} | |||
stroke={serie.stack ? 'rgba(0,0,0,0)' : serie.color} |
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.
stroke has to be transparent for a stacked area chart, because we stick a line chart on top of it to achieve the look that we want.
@@ -151,6 +191,12 @@ export class InnerCustomPlot extends PureComponent { | |||
tickFormatX={this.props.tickFormatX} | |||
/> | |||
|
|||
{stacked | |||
? React.cloneElement(staticPlot, { |
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.
AFAICT, there's only one way to achieve the look that we want (stroke on just one side of the area), and that is by drawing another line chart on top of it. Seems overkill, but it's the recommendation from the docs:
To create a chart that has a fill, no distinct lines to the left, right or bottom, but a different line style at the top, you may create an area chart with a line chart on top.
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's unfortunate, but tolerable as long as there's not any noticeable drag to page performance.
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.
@dgieselaar Doesn't this duplicate the the XAxis, YAxis and HorizontalGridLines? What about just adding an additional LineSeries to the chart?
setTime: (time: number | null) => unknown; | ||
} | null>(null); | ||
|
||
const ChartsTimeContextProvider: React.FC = ({ children }) => { |
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.
Simply syncs time across all charts in its context.
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 sounds similar to
https://github.com/elastic/kibana/blob/7056f6143d7863c2b48ff22ee376402c932bbb60/x-pack/legacy/plugins/apm/public/components/shared/charts/SyncChartGroup/index.tsx
Can those to be combined or do they serve different purposes?
@@ -5,7 +5,10 @@ | |||
*/ | |||
|
|||
import { getTransactionBreakdown } from '.'; | |||
// using .ts files results in OOM during typecheck :) |
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.
maybe we can use .json
files here too?
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.
Changed to .json
|
||
return breakdowns; | ||
const filteredTimeseriesPerSubtype = pick( |
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.
Ensures timeseries that are not in the top 20 KPIs are filtered out.
💔 Build Failed |
7b6608f
to
06b9ea7
Compare
@dgieselaar A few UI problems I've found in relation to the chart. It seems that the lines are pushed to the left (outside the x-axis area) every now and then. I'm running 15 minutes date range here, not sure how it might be related. Can we add some space / margin on those labels or value text to split them a little apart in this list? |
💚 Build Succeeded |
e5befc2
to
e59ec19
Compare
@formgeist both should be fixed now, thanks for catching this! |
e59ec19
to
bd659c0
Compare
💚 Build Succeeded |
@@ -14,5 +14,17 @@ export interface RectCoordinate { | |||
x0: number; | |||
} | |||
|
|||
export interface TimeSerie { |
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.
is this spelling of TimeSerie
intentional or should it be TimeSeries
?
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.
fixed, changed to TimeSeries
value: serviceName | ||
} | ||
} | ||
}, | ||
{ | ||
term: { | ||
'transaction.type.keyword': { | ||
'transaction.type': { |
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.
constants for 'transaction.type'
and 'transaction.name'
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.
changed all of the field names to constants from common/elasticsearch_fieldnames.ts
bd659c0
to
556c3a6
Compare
💚 Build Succeeded |
x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx
Outdated
Show resolved
Hide resolved
const noHits = series.every( | ||
serie => | ||
serie.data.filter(value => 'y' in value && value.y !== null).length === 0 | ||
); |
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 the rest of the code base noHits
refers to whether Elasticsearch actually returned any hits. We cannot just look at series.data
because we might populate that with empty state data so the graph draws the x and y-axis.
Therefore other chart endpoints return the series.totalHits
(coming from ES hits.total
):
totalHits: hits.total, |
I think we should do the same here if that makes sense to you?
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 would be more consistent, but I have some doubts about using hits.total to determine whether there is no data. Gut tells me there's a lot of cases where that will be a false positive. Using the actual data passed to the charts to determine if there are any data points by filtering out all non-null values feels like a better heuristic to me. Does that make sense?
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.
Gut tells me there's a lot of cases where that will be a false positive. Using the actual data passed to the charts to determine if there are any data points by filtering out all non-null values feels like a better heuristic to me. Does that make sense?
That does make sense. My originally assumption was that if there are any hits then the aggregation will contain (timeseries) data, and it's been true for the aggs we do for the transaction charts. But this doesn't hold true for things like derivative aggs which requires at least two data points. So using hits.total
as an indicator for data might not be a good idea in general.
If we go the direction you suggest (with a custom check) we should get rid of the noHits
, right? Eg:
- https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts#L55
- https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/MetricsChart.tsx#L37
I think it might make sense to go over how we handle this across the different visualizations. It seems like a pretty standard thing for a chart to be able to know whether it has data or not, and would be great if we can stick to one approach.
...plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx
Outdated
Show resolved
Hide resolved
…ent unnecessary re-renders
56865ed
to
714ec19
Compare
714ec19
to
c808e71
Compare
💚 Build Succeeded |
* [APM] Transaction breakdown graphs Closes elastic#36441. * Don't use .keyword now that mapping is correct * Use better heuristic for determining noHits; wrap App in memo to prevent unnecessary re-renders * Remove nested panel around TransactionBreakdown * Fix display issues in charts * Address review feedback * Address additional review feedback
* [APM] Transaction breakdown graphs Closes #36441. * Don't use .keyword now that mapping is correct * Use better heuristic for determining noHits; wrap App in memo to prevent unnecessary re-renders * Remove nested panel around TransactionBreakdown * Fix display issues in charts * Address review feedback * Address additional review feedback
const xMax = d3.max(flattenedCoordinates, d => d.x); | ||
const xMin = start ? start : d3.min(flattenedCoordinates, d => d.x); | ||
|
||
const xMax = end ? end : d3.max(flattenedCoordinates, d => d.x); |
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.
Shouldn't start
be called xMin
and end
be called xMax
?
XY_WIDTH: width | ||
XY_HEIGHT: height || XY_HEIGHT, | ||
XY_WIDTH: width, | ||
...(stacked ? { stackBy: 'y' } : {}) |
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.
Does this need to be conditional? Doesn't it only take effect when things are stacked? So setting it to stackBy: 'y'
only affects stacked
charts.
}; | ||
} | ||
|
||
export function SharedPlot({ plotValues, ...props }) { | ||
const { XY_HEIGHT: height, XY_MARGIN: margin, XY_WIDTH: width } = plotValues; |
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'm generally not a fan of renaming thing like this. If the names are not good enough, why not fix them at the root? Instead we end up with multiple names for the same thing.
() => { | ||
return timeseries.map(timeseriesConfig => { | ||
return { | ||
title: timeseriesConfig.name, |
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.
Why is this rename necessary? Shouldn't timeseriesConfig.name
be timeseriesConfig.title
in the first place? Same with timeseriesConfig.values
- shouldn't that be timeseriesConfig.data
in the first place?
I actually prefer values
over data
but then we should change that everywhere.
} | ||
}); | ||
|
||
const { timeseries_per_subtype: timeseriesPerSubtype } = 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.
Why are we using snake case in the first place? All APM endpoints return camel cased data currently.
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.
timeseries_per_subtype: timeseriesPerSubtype |
|
||
const breakdown = kpis.find( | ||
b => b.name === legend.name | ||
) as typeof kpis[0]; |
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.
What if no matches are found? Then breakdown
will be undefined
and cause a runtime error below - or is that not logically possible?
|
||
const { | ||
data, | ||
status, | ||
receivedDataDuringLifetime | ||
} = useTransactionBreakdown(); | ||
|
||
const kpis = useMemo( | ||
const kpis = data ? data.kpis : undefined; | ||
const timeseriesPerSubtype = data ? data.timeseries_per_subtype : undefined; |
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 see this access checks a lot of places which makes the code a bit harder to read. What about setting a default response like we do in:
const INITIAL_DATA: MetricsChartsByAgentAPIResponse = { | |
charts: [] | |
}; |
Could be something like:
const INITIAL_DATA: TransactionBreakdownAPIResponse = {
kpis: [],
timeseries_per_subtype: null
}
timeseries: Array<{ | ||
name: string; | ||
color: string; | ||
values: Array<{ x: number; y: number | null }>; |
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 think this should be consistent with the other charts we have (name
-> title
and values
-> data
).
As mentioned somewhere else I prefer values
over data
but might be a bigger job to change that everywhere.
Instead of having several different interfaces can we make a single timeseries interface that is re-used?
title: React.ReactNode; | ||
titleShort?: React.ReactNode; | ||
data: Array<Coordinate | RectCoordinate>; | ||
type: string; |
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.
Isn't this a duplicate of
kibana/x-pack/legacy/plugins/apm/typings/timeseries.ts
Lines 17 to 27 in 1e1153e
export interface TimeSeries { | |
title: string; | |
titleShort?: string; | |
hideLegend?: boolean; | |
hideTooltipValue?: boolean; | |
data: Array<Coordinate | RectCoordinate>; | |
legendValue?: string; | |
type: string; | |
color: string; | |
areaColor?: string; | |
} |
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.
Or rather: can they be combined?
Closes #36441.
To test this with the apm-integration-testing server, take the following steps:
docker/apm-server/Dockerfile
ENV setup.template.overwrite=true
belowFROM ${apm_server_base_image}
on line 23~/dev/apm-integration-testing/scripts/compose.py start master --with-opbeans-java --opbeans-java-agent-branch=pr/588/head --force-build --no-kibana
opbeans-java
Definition of Done for APM UI