[APM] Performance marks for RUM agent (#20931)

* [APM] Performance marks for agent

* Fixed formatting and design

* Update snapshot

* Fixed tooltip id
This commit is contained in:
Søren Louv-Jansen 2018-07-23 14:15:59 +02:00 committed by GitHub
parent bf6cc70d24
commit 7266349e33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 454 additions and 65 deletions

View file

@ -30,3 +30,5 @@ export const ERROR_CULPRIT = 'error.culprit';
export const ERROR_LOG_MESSAGE = 'error.log.message';
export const ERROR_EXC_MESSAGE = 'error.exception.message';
export const ERROR_EXC_HANDLED = 'error.exception.handled';
export const USER_ID = 'context.user.id';

View file

@ -29,7 +29,8 @@ import {
SERVICE_NAME,
ERROR_GROUP_ID,
SERVICE_AGENT_NAME,
SERVICE_LANGUAGE_NAME
SERVICE_LANGUAGE_NAME,
USER_ID
} from '../../../../../common/constants';
import { fromQuery, toQuery, history } from '../../../../utils/url';
@ -101,8 +102,8 @@ function DetailView({ errorGroup, urlParams, location }) {
},
{
label: 'User ID',
fieldName: 'context.user.id',
val: get(errorGroup.data, 'error.context.user.id', 'N/A')
fieldName: USER_ID,
val: get(errorGroup.data.error, USER_ID, 'N/A')
}
];

View file

@ -50,7 +50,7 @@ export default function TimelineHeader({ legends, transactionName }) {
</TooltipOverlay>
<Legends>
{legends.map(({ color, label }) => (
<Legend clickable={false} key={color} color={color} text={label} />
<Legend key={color} color={color} text={label} />
))}
</Legends>
</TimelineHeaderContainer>

View file

@ -43,7 +43,13 @@ const TIMELINE_MARGINS = {
class Spans extends PureComponent {
render() {
const { agentName, urlParams, location, droppedSpans } = this.props;
const {
agentName,
urlParams,
location,
droppedSpans,
agentMarks
} = this.props;
return (
<SpansRequest
urlParams={urlParams}
@ -81,6 +87,7 @@ class Spans extends PureComponent {
transactionName={urlParams.transactionName}
/>
}
agentMarks={agentMarks}
duration={totalDuration}
height={timelineHeight}
margins={TIMELINE_MARGINS}
@ -176,6 +183,7 @@ function getPrimaryType(type) {
}
Spans.propTypes = {
agentMarks: PropTypes.array,
agentName: PropTypes.string.isRequired,
droppedSpans: PropTypes.number.isRequired,
location: PropTypes.object.isRequired,

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { getAgentMarks } from '../view';
describe('TransactionDetailsView', () => {
describe('getAgentMarks', () => {
it('should be sorted', () => {
const transaction = {
transaction: {
marks: {
agent: {
domInteractive: 117,
timeToFirstByte: 10,
domComplete: 118
}
}
}
};
expect(getAgentMarks(transaction)).toEqual([
{ name: 'timeToFirstByte', timeLabel: 10000, timeAxis: 10000 },
{ name: 'domInteractive', timeLabel: 117000, timeAxis: 117000 },
{ name: 'domComplete', timeLabel: 118000, timeAxis: 118000 }
]);
});
it('should ensure they are not too close', () => {
const transaction = {
transaction: {
duration: {
us: 1000 * 1000
},
marks: {
agent: {
a: 0,
b: 10,
c: 11,
d: 12,
e: 968,
f: 969,
timeToFirstByte: 970,
domInteractive: 980,
domComplete: 990
}
}
}
};
expect(getAgentMarks(transaction)).toEqual([
{ timeLabel: 0, name: 'a', timeAxis: 0 },
{ timeLabel: 10000, name: 'b', timeAxis: 20000 },
{ timeLabel: 11000, name: 'c', timeAxis: 40000 },
{ timeLabel: 12000, name: 'd', timeAxis: 60000 },
{ timeLabel: 968000, name: 'e', timeAxis: 910000 },
{ timeLabel: 969000, name: 'f', timeAxis: 930000 },
{ timeLabel: 970000, name: 'timeToFirstByte', timeAxis: 950000 },
{ timeLabel: 980000, name: 'domInteractive', timeAxis: 970000 },
{ timeLabel: 990000, name: 'domComplete', timeAxis: 990000 }
]);
});
});
});

View file

@ -15,7 +15,7 @@ import {
borderRadius
} from '../../../../style/variables';
import { Tab, HeaderMedium } from '../../../shared/UIComponents';
import { isEmpty, capitalize, get } from 'lodash';
import { isEmpty, capitalize, get, sortBy, last } from 'lodash';
import { ContextProperties } from '../../../shared/ContextProperties';
import {
@ -27,7 +27,10 @@ import DiscoverButton from '../../../shared/DiscoverButton';
import {
TRANSACTION_ID,
PROCESSOR_EVENT,
SERVICE_AGENT_NAME
SERVICE_AGENT_NAME,
TRANSACTION_DURATION,
TRANSACTION_RESULT,
USER_ID
} from '../../../../../common/constants';
import { fromQuery, toQuery, history } from '../../../../utils/url';
import { asTime } from '../../../../utils/formatters';
@ -62,6 +65,48 @@ const PropertiesTableContainer = styled.div`
const DEFAULT_TAB = 'timeline';
export function getAgentMarks(transaction) {
const duration = get(transaction, TRANSACTION_DURATION);
const threshold = duration / 100 * 2;
return sortBy(
Object.entries(get(transaction, 'transaction.marks.agent', [])),
'1'
)
.map(([name, ms]) => ({
name,
timeLabel: ms * 1000,
timeAxis: ms * 1000
}))
.reduce((acc, curItem) => {
const prevTime = get(last(acc), 'timeAxis');
const nextValidTime = prevTime + threshold;
const isTooClose = prevTime != null && nextValidTime > curItem.timeAxis;
const canFit = nextValidTime <= duration;
if (isTooClose && canFit) {
acc.push({ ...curItem, timeAxis: nextValidTime });
} else {
acc.push(curItem);
}
return acc;
}, [])
.reduceRight((acc, curItem) => {
const prevTime = get(last(acc), 'timeAxis');
const nextValidTime = prevTime - threshold;
const isTooClose = prevTime != null && nextValidTime < curItem.timeAxis;
const canFit = nextValidTime >= 0;
if (isTooClose && canFit) {
acc.push({ ...curItem, timeAxis: nextValidTime });
} else {
acc.push(curItem);
}
return acc;
}, [])
.reverse();
}
// Ensure the selected tab exists or use the default
function getCurrentTab(tabs = [], detailTab) {
return tabs.includes(detailTab) ? detailTab : DEFAULT_TAB;
@ -86,22 +131,22 @@ function Transaction({ transaction, location, urlParams }) {
const timestamp = get(transaction, '@timestamp');
const url = get(transaction, 'context.request.url.full', 'N/A');
const duration = get(transaction, 'transaction.duration.us');
const duration = get(transaction, TRANSACTION_DURATION);
const stickyProperties = [
{
label: 'Duration',
fieldName: 'transaction.duration.us',
fieldName: TRANSACTION_DURATION,
val: duration ? asTime(duration) : 'N/A'
},
{
label: 'Result',
fieldName: 'transaction.result',
val: get(transaction, 'transaction.result', 'N/A')
fieldName: TRANSACTION_RESULT,
val: get(transaction, TRANSACTION_RESULT, 'N/A')
},
{
label: 'User ID',
fieldName: 'context.user.id',
val: get(transaction, 'context.user.id', 'N/A')
fieldName: USER_ID,
val: get(transaction, USER_ID, 'N/A')
}
];
@ -168,6 +213,7 @@ function Transaction({ transaction, location, urlParams }) {
{currentTab === DEFAULT_TAB ? (
<Spans
agentName={agentName}
agentMarks={getAgentMarks(transaction)}
droppedSpans={get(
transaction,
'transaction.spanCount.dropped.total',

View file

@ -1271,7 +1271,7 @@ Array [
align-items: center;
font-size: 12px;
color: #666666;
cursor: pointer;
cursor: initial;
opacity: 1;
-webkit-user-select: none;
-moz-user-select: none;

View file

@ -30,21 +30,21 @@ export default class Legend extends PureComponent {
render() {
const {
onClick,
color,
text,
color = colors.apmBlue,
fontSize = fontSizes.small,
radius = units.minus - 1,
disabled = false,
clickable = true,
className
clickable = false,
...rest
} = this.props;
return (
<Container
onClick={onClick}
disabled={disabled}
clickable={clickable}
clickable={clickable || Boolean(onClick)}
fontSize={fontSize}
className={className}
{...rest}
>
<Indicator color={color} radius={radius} />
{text}

View file

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiToolTip } from '@elastic/eui';
import Legend from '../Legend';
import { colors, units, px } from '../../../../style/variables';
import styled from 'styled-components';
import { asTime } from '../../../../utils/formatters';
const NameContainer = styled.div`
border-bottom: 1px solid ${colors.gray3};
padding-bottom: ${px(units.half)};
`;
const TimeContainer = styled.div`
color: ${colors.gray3};
padding-top: ${px(units.half)};
`;
export default function AgentMarker({ agentMark, x }) {
const legendWidth = 11;
return (
<div
style={{
position: 'absolute',
left: px(x - legendWidth / 2)
}}
>
<EuiToolTip
id={agentMark.name}
position="top"
content={
<div>
<NameContainer>{agentMark.name}</NameContainer>
<TimeContainer>{asTime(agentMark.timeLabel)}</TimeContainer>
</div>
}
>
<Legend clickable color={colors.gray3} />
</EuiToolTip>
</div>
);
}
AgentMarker.propTypes = {
agentMark: PropTypes.object.isRequired,
x: PropTypes.number.isRequired
};

View file

@ -5,10 +5,12 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import { Sticky } from 'react-sticky';
import { XYPlot, XAxis } from 'react-vis';
import LastTickValue from './LastTickValue';
import AgentMarker from './AgentMarker';
import { colors, px } from '../../../../style/variables';
import { getTimeFormatter } from '../../../../utils/formatters';
@ -16,7 +18,7 @@ import { getTimeFormatter } from '../../../../utils/formatters';
const getXAxisTickValues = (tickValues, xMax) =>
_.last(tickValues) * 1.05 > xMax ? tickValues.slice(0, -1) : tickValues;
function TimelineAxis({ header, plotValues }) {
function TimelineAxis({ header, plotValues, agentMarks }) {
const { margins, tickValues, width, xDomain, xMax, xScale } = plotValues;
const tickFormat = getTimeFormatter(xMax);
const xAxisTickValues = getXAxisTickValues(tickValues, xMax);
@ -60,6 +62,14 @@ function TimelineAxis({ header, plotValues }) {
/>
<LastTickValue x={xScale(xMax)} value={tickFormat(xMax)} />
{agentMarks.map(agentMark => (
<AgentMarker
key={agentMark.timeAxis}
agentMark={agentMark}
x={xScale(agentMark.timeAxis)}
/>
))}
</XYPlot>
</div>
);
@ -68,4 +78,14 @@ function TimelineAxis({ header, plotValues }) {
);
}
TimelineAxis.propTypes = {
header: PropTypes.node,
plotValues: PropTypes.object.isRequired,
agentMarks: PropTypes.array
};
TimelineAxis.defaultProps = {
agentMarks: []
};
export default TimelineAxis;

View file

@ -5,10 +5,11 @@
*/
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { XYPlot, VerticalGridLines } from 'react-vis';
import { colors } from '../../../../style/variables';
export default class VerticalLines extends PureComponent {
class VerticalLines extends PureComponent {
render() {
const {
width,
@ -19,6 +20,10 @@ export default class VerticalLines extends PureComponent {
xMax
} = this.props.plotValues;
const agentMarkTimes = this.props.agentMarks.map(
({ timeAxis }) => timeAxis
);
return (
<div
style={{
@ -38,8 +43,9 @@ export default class VerticalLines extends PureComponent {
tickValues={tickValues}
style={{ stroke: colors.gray5 }}
/>
<VerticalGridLines
tickValues={[xMax]}
tickValues={[...agentMarkTimes, xMax]}
style={{ stroke: colors.gray3 }}
/>
</XYPlot>
@ -47,3 +53,14 @@ export default class VerticalLines extends PureComponent {
);
}
}
VerticalLines.propTypes = {
plotValues: PropTypes.object.isRequired,
agentMarks: PropTypes.array
};
VerticalLines.defaultProps = {
agentMarks: []
};
export default VerticalLines;

View file

@ -1,6 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Timeline should render with data 1`] = `
.c0 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
font-size: 12px;
color: #666666;
cursor: pointer;
opacity: 1;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.c1 {
width: 11px;
height: 11px;
margin-right: 5.5px;
background: #999999;
border-radius: 100%;
}
<div
onScroll={[Function]}
onTouchEnd={[Function]}
@ -38,7 +65,7 @@ exports[`Timeline should render with data 1`] = `
style={
Object {
"height": "40px",
"width": "100px",
"width": "1000px",
}
}
>
@ -52,7 +79,7 @@ exports[`Timeline should render with data 1`] = `
onMouseLeave={[Function]}
onMouseMove={[Function]}
onWheel={[Function]}
width={100}
width={1000}
>
<g
className="rv-xy-plot__axis rv-xy-plot__axis--horizontal "
@ -120,7 +147,7 @@ exports[`Timeline should render with data 1`] = `
},
}
}
transform="translate(0, 0)"
transform="translate(87.9408646541237, 0)"
>
<line
className="rv-xy-plot__axis__tick__line"
@ -162,7 +189,7 @@ exports[`Timeline should render with data 1`] = `
},
}
}
transform="translate(0, 0)"
transform="translate(175.8817293082474, 0)"
>
<line
className="rv-xy-plot__axis__tick__line"
@ -204,7 +231,7 @@ exports[`Timeline should render with data 1`] = `
},
}
}
transform="translate(0, 0)"
transform="translate(263.82259396237106, 0)"
>
<line
className="rv-xy-plot__axis__tick__line"
@ -246,7 +273,7 @@ exports[`Timeline should render with data 1`] = `
},
}
}
transform="translate(0, 0)"
transform="translate(351.7634586164948, 0)"
>
<line
className="rv-xy-plot__axis__tick__line"
@ -288,7 +315,7 @@ exports[`Timeline should render with data 1`] = `
},
}
}
transform="translate(0, 0)"
transform="translate(439.7043232706185, 0)"
>
<line
className="rv-xy-plot__axis__tick__line"
@ -330,7 +357,7 @@ exports[`Timeline should render with data 1`] = `
},
}
}
transform="translate(0, 0)"
transform="translate(527.6451879247421, 0)"
>
<line
className="rv-xy-plot__axis__tick__line"
@ -372,7 +399,7 @@ exports[`Timeline should render with data 1`] = `
},
}
}
transform="translate(0, 0)"
transform="translate(615.5860525788659, 0)"
>
<line
className="rv-xy-plot__axis__tick__line"
@ -414,7 +441,7 @@ exports[`Timeline should render with data 1`] = `
},
}
}
transform="translate(0, 0)"
transform="translate(703.5269172329896, 0)"
>
<line
className="rv-xy-plot__axis__tick__line"
@ -456,7 +483,7 @@ exports[`Timeline should render with data 1`] = `
},
}
}
transform="translate(0, 0)"
transform="translate(791.4677818871132, 0)"
>
<line
className="rv-xy-plot__axis__tick__line"
@ -492,7 +519,7 @@ exports[`Timeline should render with data 1`] = `
</g>
</g>
<g
transform="translate(50, 40)"
transform="translate(950, 40)"
>
<text
dy="0"
@ -503,6 +530,93 @@ exports[`Timeline should render with data 1`] = `
</text>
</g>
</svg>
<div
style={
Object {
"left": "484.2043232706185px",
"position": "absolute",
}
}
>
<span
className="euiToolTipAnchor"
>
<div
aria-describedby="timeToFirstByte"
className="c0"
disabled={false}
fontSize="12px"
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<span
className="c1"
color="#999999"
radius={11}
/>
</div>
</span>
</div>
<div
style={
Object {
"left": "528.1747555976804px",
"position": "absolute",
}
}
>
<span
className="euiToolTipAnchor"
>
<div
aria-describedby="domInteractive"
className="c0"
disabled={false}
fontSize="12px"
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<span
className="c1"
color="#999999"
radius={11}
/>
</div>
</span>
</div>
<div
style={
Object {
"left": "879.9382142141751px",
"position": "absolute",
}
}
>
<span
className="euiToolTipAnchor"
>
<div
aria-describedby="domComplete"
className="c0"
disabled={false}
fontSize="12px"
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<span
className="c1"
color="#999999"
radius={11}
/>
</div>
</span>
</div>
</div>
</div>
</div>
@ -520,7 +634,7 @@ exports[`Timeline should render with data 1`] = `
style={
Object {
"height": "216px",
"width": "100px",
"width": "1000px",
}
}
>
@ -534,7 +648,7 @@ exports[`Timeline should render with data 1`] = `
onMouseLeave={[Function]}
onMouseMove={[Function]}
onWheel={[Function]}
width={100}
width={1000}
>
<g
className="rv-xy-plot__grid-lines"
@ -559,8 +673,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={0}
x2={0}
x1={87.9408646541237}
x2={87.9408646541237}
y1={0}
y2={116}
/>
@ -571,8 +685,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={0}
x2={0}
x1={175.8817293082474}
x2={175.8817293082474}
y1={0}
y2={116}
/>
@ -583,8 +697,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={0}
x2={0}
x1={263.82259396237106}
x2={263.82259396237106}
y1={0}
y2={116}
/>
@ -595,8 +709,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={0}
x2={0}
x1={351.7634586164948}
x2={351.7634586164948}
y1={0}
y2={116}
/>
@ -607,8 +721,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={0}
x2={0}
x1={439.7043232706185}
x2={439.7043232706185}
y1={0}
y2={116}
/>
@ -619,8 +733,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={0}
x2={0}
x1={527.6451879247421}
x2={527.6451879247421}
y1={0}
y2={116}
/>
@ -631,8 +745,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={0}
x2={0}
x1={615.5860525788659}
x2={615.5860525788659}
y1={0}
y2={116}
/>
@ -643,8 +757,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={0}
x2={0}
x1={703.5269172329896}
x2={703.5269172329896}
y1={0}
y2={116}
/>
@ -655,8 +769,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={0}
x2={0}
x1={791.4677818871132}
x2={791.4677818871132}
y1={0}
y2={116}
/>
@ -667,8 +781,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={0}
x2={0}
x1={879.408646541237}
x2={879.408646541237}
y1={0}
y2={116}
/>
@ -684,8 +798,44 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#999999",
}
}
x1={0}
x2={0}
x1={439.7043232706185}
x2={439.7043232706185}
y1={0}
y2={116}
/>
<line
className="rv-xy-plot__grid-lines__line"
style={
Object {
"stroke": "#999999",
}
}
x1={483.6747555976803}
x2={483.6747555976803}
y1={0}
y2={116}
/>
<line
className="rv-xy-plot__grid-lines__line"
style={
Object {
"stroke": "#999999",
}
}
x1={835.4382142141751}
x2={835.4382142141751}
y1={0}
y2={116}
/>
<line
className="rv-xy-plot__grid-lines__line"
style={
Object {
"stroke": "#999999",
}
}
x1={900}
x2={900}
y1={0}
y2={116}
/>

View file

@ -1,5 +1,5 @@
{
"width": 100,
"width": 1000,
"duration": 204683,
"height": 116,
"margins": {
@ -8,5 +8,10 @@
"right": 50,
"bottom": 0
},
"animation": null
"animation": null,
"agentMarks": [
{ "timeLabel": 100000, "name": "timeToFirstByte", "timeAxis": 100000 },
{ "timeLabel": 110000, "name": "domInteractive", "timeAxis": 110000 },
{ "timeLabel": 190000, "name": "domComplete", "timeAxis": 190000 }
]
}

View file

@ -23,8 +23,7 @@ class Timeline extends PureComponent {
);
render() {
const { width, duration, header } = this.props;
const { width, duration, header, agentMarks } = this.props;
if (duration == null || !width) {
return null;
}
@ -33,14 +32,19 @@ class Timeline extends PureComponent {
return (
<div>
<TimelineAxis plotValues={plotValues} header={header} />
<VerticalLines plotValues={plotValues} />
<TimelineAxis
plotValues={plotValues}
agentMarks={agentMarks}
header={header}
/>
<VerticalLines plotValues={plotValues} agentMarks={agentMarks} />
</div>
);
}
}
Timeline.propTypes = {
agentMarks: PropTypes.array,
duration: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
header: PropTypes.node,

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { asTime } from '../formatters';
describe('formatters', () => {
it('asTime', () => {
expect(asTime(1000)).toBe('1 ms');
expect(asTime(1000 * 1000)).toBe('1,000 ms');
expect(asTime(1000 * 1000 * 10)).toBe('10,000 ms');
expect(asTime(1000 * 1000 * 20)).toBe('20.0 s');
});
});

View file

@ -7,7 +7,7 @@
import { memoize } from 'lodash';
import numeral from '@elastic/numeral';
const UNIT_CUT_OFF = 10 * 1000000;
const UNIT_CUT_OFF = 10 * 1000000; // 10 seconds in microseconds
export function asSeconds(value, withUnit = true) {
const formatted = asDecimal(value / 1000000);
@ -34,6 +34,9 @@ export function timeUnit(max) {
return max > UNIT_CUT_OFF ? 's' : 'ms';
}
/*
* value: time in microseconds
*/
export function asTime(value) {
return getTimeFormatter(value)(value);
}