[7.x] [APM] Consistent flyout headers (#46312) (#46620)

* [APM] Consistent flyout headers (#46312)

* [APM] Consistent flyout headers

Closes #46078 and #46074.

* Tooltips for span flyout badges

* Review feedback

* Fix import for SERVICE_NAME in StickySpanProperties
This commit is contained in:
Dario Gieselaar 2019-09-26 22:02:17 +02:00 committed by GitHub
parent cc37c24944
commit f582da0f0a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 503 additions and 702 deletions

View file

@ -1,117 +0,0 @@
/*
* 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 { shallow, ShallowWrapper } from 'enzyme';
import React from 'react';
import { APMError } from '../../../../../typings/es_schemas/ui/APMError';
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
import { IStickyProperty } from '../../../shared/StickyProperties';
import { StickyErrorProperties } from './StickyErrorProperties';
import {
ERROR_PAGE_URL,
URL_FULL
} from '../../../../../common/elasticsearch_fieldnames';
describe('StickyErrorProperties', () => {
it('should render StickyProperties', () => {
const transaction = {
http: { request: { method: 'GET' } },
url: { full: 'myUrl' },
trace: { id: 'traceId' },
transaction: {
type: 'myTransactionType',
name: 'myTransactionName',
id: 'myTransactionName'
},
service: { name: 'myService' },
user: { id: 'myUserId' }
} as Transaction;
const error = {
'@timestamp': 'myTimestamp',
agent: { name: 'nodejs' },
http: { request: { method: 'GET' } },
url: { full: 'myUrl' },
service: { name: 'myService' },
user: { id: 'myUserId' },
error: { exception: [{ handled: true }] },
transaction: { id: 'myTransactionId', sampled: true }
} as APMError;
const wrapper = shallow(
<StickyErrorProperties error={error} transaction={transaction} />
);
expect(wrapper).toMatchSnapshot();
});
it('url.full', () => {
const error = {
agent: { name: 'nodejs' },
url: { full: 'myFullUrl' }
} as APMError;
const wrapper = shallow(
<StickyErrorProperties error={error} transaction={undefined} />
);
const urlValue = getValueByFieldName(wrapper, URL_FULL);
expect(urlValue).toBe('myFullUrl');
});
it('error.page.url', () => {
const error = {
agent: { name: 'rum-js' },
error: { page: { url: 'myPageUrl' } }
} as APMError;
const wrapper = shallow(
<StickyErrorProperties error={error} transaction={undefined} />
);
const urlValue = getValueByFieldName(wrapper, ERROR_PAGE_URL);
expect(urlValue).toBe('myPageUrl');
});
describe('error.exception.handled', () => {
it('should should render "true"', () => {
const error = {
agent: { name: 'nodejs' },
error: { exception: [{ handled: true }] }
} as APMError;
const wrapper = shallow(
<StickyErrorProperties error={error} transaction={undefined} />
);
const value = getValueByFieldName(wrapper, 'error.exception.handled');
expect(value).toBe('true');
});
it('should should render "false"', () => {
const error = {
agent: { name: 'nodejs' },
error: { exception: [{ handled: false }] }
} as APMError;
const wrapper = shallow(
<StickyErrorProperties error={error} transaction={undefined} />
);
const value = getValueByFieldName(wrapper, 'error.exception.handled');
expect(value).toBe('false');
});
it('should should render "N/A"', () => {
const error = { agent: { name: 'nodejs' } } as APMError;
const wrapper = shallow(
<StickyErrorProperties error={error} transaction={undefined} />
);
const value = getValueByFieldName(wrapper, 'error.exception.handled');
expect(value).toBe('N/A');
});
});
});
function getValueByFieldName(wrapper: ShallowWrapper, fieldName: string) {
const stickyProps = wrapper.prop('stickyProperties') as IStickyProperty[];
const prop = stickyProps.find(p => p.fieldName === fieldName);
return prop && prop.val;
}

View file

@ -1,126 +0,0 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { isBoolean } from 'lodash';
import React, { Fragment } from 'react';
import { idx } from '@kbn/elastic-idx';
import {
ERROR_EXC_HANDLED,
HTTP_REQUEST_METHOD,
TRANSACTION_ID,
URL_FULL,
USER_ID,
ERROR_PAGE_URL
} from '../../../../../common/elasticsearch_fieldnames';
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
import { APMError } from '../../../../../typings/es_schemas/ui/APMError';
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
import { StickyProperties } from '../../../shared/StickyProperties';
import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink';
import { isRumAgentName } from '../../../../../common/agent_name';
interface Props {
error: APMError;
transaction: Transaction | undefined;
}
function TransactionLinkWrapper({
transaction
}: {
transaction: Transaction | undefined;
}) {
if (!transaction) {
return <Fragment>{NOT_AVAILABLE_LABEL}</Fragment>;
}
const isSampled = transaction.transaction.sampled;
if (!isSampled) {
return <Fragment>{transaction.transaction.id}</Fragment>;
}
return (
<TransactionDetailLink
serviceName={transaction.service.name}
transactionId={transaction.transaction.id}
traceId={transaction.trace.id}
transactionName={transaction.transaction.name}
transactionType={transaction.transaction.type}
>
{transaction.transaction.id}
</TransactionDetailLink>
);
}
export function StickyErrorProperties({ error, transaction }: Props) {
const isHandled = idx(error, _ => _.error.exception[0].handled);
const isRumAgent = isRumAgentName(error.agent.name);
const { urlFieldName, urlValue } = isRumAgent
? {
urlFieldName: ERROR_PAGE_URL,
urlValue: idx(error, _ => _.error.page.url)
}
: {
urlFieldName: URL_FULL,
urlValue: idx(error, _ => _.url.full)
};
const stickyProperties = [
{
fieldName: '@timestamp',
label: i18n.translate('xpack.apm.errorGroupDetails.timestampLabel', {
defaultMessage: 'Timestamp'
}),
val: error['@timestamp'],
width: '50%'
},
{
fieldName: urlFieldName,
label: 'URL',
val: urlValue || NOT_AVAILABLE_LABEL,
truncated: true,
width: '50%'
},
{
fieldName: HTTP_REQUEST_METHOD,
label: i18n.translate('xpack.apm.errorGroupDetails.requestMethodLabel', {
defaultMessage: 'Request method'
}),
val: idx(error, _ => _.http.request.method) || NOT_AVAILABLE_LABEL,
width: '25%'
},
{
fieldName: ERROR_EXC_HANDLED,
label: i18n.translate('xpack.apm.errorGroupDetails.handledLabel', {
defaultMessage: 'Handled'
}),
val: isBoolean(isHandled) ? String(isHandled) : NOT_AVAILABLE_LABEL,
width: '25%'
},
{
fieldName: TRANSACTION_ID,
label: i18n.translate(
'xpack.apm.errorGroupDetails.transactionSampleIdLabel',
{
defaultMessage: 'Transaction sample ID'
}
),
val: <TransactionLinkWrapper transaction={transaction} />,
width: '25%'
},
{
fieldName: USER_ID,
label: i18n.translate('xpack.apm.errorGroupDetails.userIdLabel', {
defaultMessage: 'User ID'
}),
val: idx(error, _ => _.user.id) || NOT_AVAILABLE_LABEL,
width: '25%'
}
];
return <StickyProperties stickyProperties={stickyProperties} />;
}

View file

@ -1,74 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StickyErrorProperties should render StickyProperties 1`] = `
<StickyProperties
stickyProperties={
Array [
Object {
"fieldName": "@timestamp",
"label": "Timestamp",
"val": "myTimestamp",
"width": "50%",
},
Object {
"fieldName": "url.full",
"label": "URL",
"truncated": true,
"val": "myUrl",
"width": "50%",
},
Object {
"fieldName": "http.request.method",
"label": "Request method",
"val": "GET",
"width": "25%",
},
Object {
"fieldName": "error.exception.handled",
"label": "Handled",
"val": "true",
"width": "25%",
},
Object {
"fieldName": "transaction.id",
"label": "Transaction sample ID",
"val": <TransactionLinkWrapper
transaction={
Object {
"http": Object {
"request": Object {
"method": "GET",
},
},
"service": Object {
"name": "myService",
},
"trace": Object {
"id": "traceId",
},
"transaction": Object {
"id": "myTransactionName",
"name": "myTransactionName",
"type": "myTransactionType",
},
"url": Object {
"full": "myUrl",
},
"user": Object {
"id": "myUserId",
},
}
}
/>,
"width": "25%",
},
Object {
"fieldName": "user.id",
"label": "User ID",
"val": "myUserId",
"width": "25%",
},
]
}
/>
`;

View file

@ -4,7 +4,6 @@ exports[`DetailView should render Discover button 1`] = `
<DiscoverErrorLink
error={
Object {
"@timestamp": "myTimestamp",
"error": Object {
"exception": Object {
"handled": true,
@ -18,6 +17,9 @@ exports[`DetailView should render Discover button 1`] = `
"service": Object {
"name": "myService",
},
"timestamp": Object {
"us": 0,
},
"transaction": Object {
"id": "myTransactionId",
"sampled": true,
@ -49,8 +51,10 @@ exports[`DetailView should render TabContent 1`] = `
}
error={
Object {
"@timestamp": "myTimestamp",
"context": Object {},
"timestamp": Object {
"us": 0,
},
}
}
/>

View file

@ -34,7 +34,9 @@ describe('DetailView', () => {
occurrencesCount: 10,
transaction: undefined,
error: {
'@timestamp': 'myTimestamp',
timestamp: {
us: 0
},
http: { request: { method: 'GET' } },
url: { full: 'myUrl' },
service: { name: 'myService' },
@ -56,10 +58,14 @@ describe('DetailView', () => {
expect(wrapper).toMatchSnapshot();
});
it('should render StickyProperties', () => {
it('should render a Summary', () => {
const errorGroup = {
occurrencesCount: 10,
error: {} as any,
error: {
timestamp: {
us: 0
}
} as any,
transaction: undefined
};
const wrapper = shallow(
@ -68,7 +74,7 @@ describe('DetailView', () => {
urlParams={{}}
location={{} as Location}
/>
).find('StickyErrorProperties');
).find('Summary');
expect(wrapper.exists()).toBe(true);
});
@ -78,7 +84,9 @@ describe('DetailView', () => {
occurrencesCount: 10,
transaction: undefined,
error: {
'@timestamp': 'myTimestamp',
timestamp: {
us: 0
},
service: {},
user: {}
} as any
@ -100,7 +108,9 @@ describe('DetailView', () => {
occurrencesCount: 10,
transaction: undefined,
error: {
'@timestamp': 'myTimestamp',
timestamp: {
us: 0
},
context: {}
} as any
};

View file

@ -10,7 +10,9 @@ import {
EuiSpacer,
EuiTab,
EuiTabs,
EuiTitle
EuiTitle,
EuiIcon,
EuiToolTip
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Location } from 'history';
@ -21,7 +23,7 @@ import { idx } from '@kbn/elastic-idx';
import { ErrorGroupAPIResponse } from '../../../../../server/lib/errors/get_error_group';
import { APMError } from '../../../../../typings/es_schemas/ui/APMError';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { px, unit } from '../../../../style/variables';
import { px, unit, units } from '../../../../style/variables';
import { DiscoverErrorLink } from '../../../shared/Links/DiscoverLinks/DiscoverErrorLink';
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
import { history } from '../../../../utils/history';
@ -33,7 +35,10 @@ import {
getTabs,
logStacktraceTab
} from './ErrorTabs';
import { StickyErrorProperties } from './StickyErrorProperties';
import { Summary } from '../../../shared/Summary';
import { TimestampSummaryItem } from '../../../shared/Summary/TimestampSummaryItem';
import { HttpInfoSummaryItem } from '../../../shared/Summary/HttpInfoSummaryItem';
import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink';
const HeaderContainer = styled.div`
display: flex;
@ -42,6 +47,12 @@ const HeaderContainer = styled.div`
margin-bottom: ${px(unit)};
`;
const TransactionLinkName = styled.div`
margin-left: ${px(units.half)};
display: inline-block;
vertical-align: middle;
`;
interface Props {
errorGroup: ErrorGroupAPIResponse;
urlParams: IUrlParams;
@ -67,6 +78,12 @@ export function DetailView({ errorGroup, urlParams, location }: Props) {
const tabs = getTabs(error);
const currentTab = getCurrentTab(tabs, urlParams.detailTab);
const errorUrl =
idx(error, _ => _.error.page.url) || idx(error, _ => _.url.full);
const method = idx(error, _ => _.http.request.method);
const status = idx(error, _ => _.http.response.status_code);
return (
<EuiPanel>
<HeaderContainer>
@ -94,7 +111,41 @@ export function DetailView({ errorGroup, urlParams, location }: Props) {
</DiscoverErrorLink>
</HeaderContainer>
<StickyErrorProperties error={error} transaction={transaction} />
<Summary
items={[
<TimestampSummaryItem time={error.timestamp.us / 1000} />,
errorUrl && method ? (
<HttpInfoSummaryItem
url={errorUrl}
method={method}
status={status}
/>
) : null,
transaction && (
<EuiToolTip
content={i18n.translate(
'xpack.apm.errorGroupDetails.relatedTransactionSample',
{
defaultMessage: 'Related transaction sample'
}
)}
>
<TransactionDetailLink
traceId={transaction.trace.id}
transactionId={transaction.transaction.id}
transactionName={transaction.transaction.name}
transactionType={transaction.transaction.type}
serviceName={transaction.service.name}
>
<EuiIcon type="merge" />
<TransactionLinkName>
{transaction.transaction.name}
</TransactionLinkName>
</TransactionDetailLink>
</EuiToolTip>
)
]}
/>
<EuiSpacer />

View file

@ -9,28 +9,30 @@ import { i18n } from '@kbn/i18n';
import { EuiToolTip } from '@elastic/eui';
import { asPercent } from '../../../../utils/formatters';
interface PercentOfTraceProps {
interface PercentOfParentProps {
duration: number;
totalDuration?: number;
parentType: 'trace' | 'transaction';
}
export function PercentOfTrace({
export function PercentOfParent({
duration,
totalDuration
}: PercentOfTraceProps) {
totalDuration,
parentType
}: PercentOfParentProps) {
totalDuration = totalDuration || duration;
const isOver100 = duration > totalDuration;
const percentOfTrace = isOver100
const percentOfParent = isOver100
? '>100%'
: asPercent(duration, totalDuration, '');
const percentOfTraceText = i18n.translate(
'xpack.apm.transactionDetails.percentOfTrace',
{
defaultMessage: '{value} of trace',
values: { value: percentOfTrace }
}
);
const percentOfParentText = i18n.translate('xpack.apm.percentOfParent', {
defaultMessage:
'({value} of {parentType, select, transaction { transaction } trace {trace} })',
values: { value: percentOfParent, parentType }
});
const childType = parentType === 'trace' ? 'transaction' : 'span';
return (
<>
@ -40,14 +42,18 @@ export function PercentOfTrace({
'xpack.apm.transactionDetails.percentOfTraceLabelExplanation',
{
defaultMessage:
'The % of trace exceeds 100% because this transaction takes longer than the root transaction.'
'The % of {parentType, select, transaction {transaction} trace {trace} } exceeds 100% because this {childType, select, span {span} transaction {transaction} } takes longer than the root transaction.',
values: {
parentType,
childType
}
}
)}
>
<>{percentOfTraceText}</>
<>{percentOfParentText}</>
</EuiToolTip>
) : (
`(${percentOfTraceText})`
percentOfParentText
)}
</>
);

View file

@ -25,7 +25,7 @@ import {
import { ErrorCountBadge } from './ErrorCountBadge';
import { isRumAgentName } from '../../../../../common/agent_name';
import { fontSize } from '../../../../style/variables';
import { PercentOfTrace } from './PercentOfTrace';
import { PercentOfParent } from './PercentOfParent';
interface Props {
transaction: Transaction;
@ -96,7 +96,13 @@ export function StickyTransactionProperties({
defaultMessage: '% of trace'
}
),
val: <PercentOfTrace duration={duration} totalDuration={totalDuration} />,
val: (
<PercentOfParent
duration={duration}
totalDuration={totalDuration}
parentType="trace"
/>
),
width: '25%'
},
{

View file

@ -1,37 +0,0 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { EuiToolTip, EuiText } from '@elastic/eui';
import styled from 'styled-components';
import { asTime } from '../../../../../utils/formatters';
import { PercentOfTrace } from '../PercentOfTrace';
interface DurationProps {
duration: number;
totalDuration?: number;
}
const Span = styled('span')`
white-space: nowrap;
`;
export function Duration({ duration, totalDuration }: DurationProps) {
totalDuration = totalDuration || duration;
const label = i18n.translate('xpack.apm.transactionDetails.durationLabel', {
defaultMessage: 'Duration'
});
return (
<Span>
<EuiToolTip content={label}>
<EuiText>{asTime(duration)}</EuiText>
</EuiToolTip>{' '}
<PercentOfTrace duration={duration} totalDuration={totalDuration} />
</Span>
);
}

View file

@ -1,30 +0,0 @@
/*
* 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 moment from 'moment';
import { EuiToolTip, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
interface TimestampProps {
transactionTimestamp: string;
}
export function Timestamp({ transactionTimestamp }: TimestampProps) {
const timestamp = moment(transactionTimestamp).format(
'MMM Do YYYY HH:mm:ss.SSS'
);
const label = i18n.translate('xpack.apm.transactionDetails.timestampLabel', {
defaultMessage: 'Timestamp'
});
return (
<EuiToolTip content={label}>
<EuiText>{timestamp}</EuiText>
</EuiToolTip>
);
}

View file

@ -1,93 +0,0 @@
/*
* 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 { EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { idx } from '@kbn/elastic-idx';
import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction';
import { isRumAgentName } from '../../../../../../common/agent_name';
import { ErrorCountBadge } from '../ErrorCountBadge';
import { Duration } from './Duration';
import { Timestamp } from './Timestamp';
import { HttpInfo } from './HttpInfo';
import { Result } from './Result';
import { px, units } from '../../../../../../public/style/variables';
// TODO: Light/Dark theme (@see https://github.com/elastic/kibana/issues/44840)
const theme = euiLightVars;
const Item = styled(EuiFlexItem)`
flex-wrap: nowrap;
border-right: 1px solid ${theme.euiColorMediumShade};
padding-right: ${px(units.half)};
&:last-child {
border-right: none;
padding-right: 0;
}
`;
interface Props {
errorCount: number;
totalDuration?: number;
transaction: Transaction;
}
export function TraceSummary({
errorCount,
totalDuration,
transaction
}: Props) {
const result = idx(transaction, _ => _.transaction.result);
const isRumAgent = isRumAgentName(transaction.agent.name);
const url = isRumAgent
? idx(transaction, _ => _.transaction.page.url)
: idx(transaction, _ => _.url.full);
return (
<>
<EuiText size="s">
<EuiFlexGroup justifyContent="flexStart" gutterSize="s" wrap={true}>
<Item grow={false}>
<Timestamp transactionTimestamp={transaction['@timestamp']} />
</Item>
<Item grow={false}>
<Duration
duration={transaction.transaction.duration.us}
totalDuration={totalDuration}
/>
</Item>
{url && (
<Item grow={false}>
<HttpInfo transaction={transaction} url={url} />
</Item>
)}
{result && !url && (
<Item grow={false}>
<Result result={result} />
</Item>
)}
{errorCount > 0 && (
<Item grow={false}>
<span>
<ErrorCountBadge title={undefined}>
{i18n.translate('xpack.apm.transactionDetails.errorCount', {
defaultMessage:
'{errorCount, number} {errorCount, plural, one {Error} other {Errors}}',
values: { errorCount }
})}
</ErrorCountBadge>
</span>
</Item>
)}
</EuiFlexGroup>
</EuiText>
</>
);
}

View file

@ -1,6 +0,0 @@
/*
* 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.
*/
export { TraceSummary } from './TraceSummary';

View file

@ -35,7 +35,7 @@ export function FlyoutTopLevelProperties({ transaction }: Props) {
{transaction.service.name}
</TransactionOverviewLink>
),
width: '50%'
width: '25%'
},
{
label: i18n.translate('xpack.apm.transactionDetails.transactionLabel', {
@ -53,7 +53,7 @@ export function FlyoutTopLevelProperties({ transaction }: Props) {
{transaction.transaction.name}
</TransactionDetailLink>
),
width: '50%'
width: '25%'
}
];

View file

@ -6,68 +6,63 @@
import { i18n } from '@kbn/i18n';
import React from 'react';
import { Transaction } from '../../../../../../../../typings/es_schemas/ui/Transaction';
import {
SPAN_ACTION,
SPAN_DURATION,
SPAN_NAME,
SPAN_SUBTYPE,
SPAN_TYPE
TRANSACTION_NAME,
SERVICE_NAME
} from '../../../../../../../../common/elasticsearch_fieldnames';
import { NOT_AVAILABLE_LABEL } from '../../../../../../../../common/i18n';
import { Span } from '../../../../../../../../typings/es_schemas/ui/Span';
import { asMillis, asPercent } from '../../../../../../../utils/formatters';
import { StickyProperties } from '../../../../../../shared/StickyProperties';
function formatType(type: string) {
switch (type) {
case 'db':
return 'DB';
case 'hard-navigation':
return i18n.translate(
'xpack.apm.transactionDetails.spanFlyout.spanType.navigationTimingLabel',
{
defaultMessage: 'Navigation timing'
}
);
default:
return type;
}
}
function formatSubtype(subtype: string) {
switch (subtype) {
case 'mysql':
return 'MySQL';
default:
return subtype;
}
}
function getSpanTypes(span: Span) {
const { type, subtype, action } = span.span;
const [primaryType, subtypeFromType, actionFromType] = type.split('.'); // This is to support 6.x data
return {
spanType: formatType(primaryType),
spanSubtype: formatSubtype(subtype || subtypeFromType),
spanAction: action || actionFromType
};
}
import { TransactionOverviewLink } from '../../../../../../shared/Links/apm/TransactionOverviewLink';
import { TransactionDetailLink } from '../../../../../../shared/Links/apm/TransactionDetailLink';
interface Props {
span: Span;
totalDuration?: number;
transaction?: Transaction;
}
export function StickySpanProperties({ span, totalDuration }: Props) {
if (!totalDuration) {
return null;
}
export function StickySpanProperties({ span, transaction }: Props) {
const spanName = span.span.name;
const spanDuration = span.span.duration.us;
const { spanType, spanSubtype, spanAction } = getSpanTypes(span);
const transactionStickyProperties = transaction
? [
{
label: i18n.translate('xpack.apm.transactionDetails.serviceLabel', {
defaultMessage: 'Service'
}),
fieldName: SERVICE_NAME,
val: (
<TransactionOverviewLink serviceName={transaction.service.name}>
{transaction.service.name}
</TransactionOverviewLink>
),
width: '25%'
},
{
label: i18n.translate(
'xpack.apm.transactionDetails.transactionLabel',
{
defaultMessage: 'Transaction'
}
),
fieldName: TRANSACTION_NAME,
val: (
<TransactionDetailLink
serviceName={transaction.service.name}
transactionId={transaction.transaction.id}
traceId={transaction.trace.id}
transactionName={transaction.transaction.name}
transactionType={transaction.transaction.type}
>
{transaction.transaction.name}
</TransactionDetailLink>
),
width: '25%'
}
]
: [];
const stickyProperties = [
{
label: i18n.translate(
@ -79,72 +74,10 @@ export function StickySpanProperties({ span, totalDuration }: Props) {
fieldName: SPAN_NAME,
val: spanName || NOT_AVAILABLE_LABEL,
truncated: true,
width: '50%'
width: '25%'
},
{
fieldName: SPAN_DURATION,
label: i18n.translate(
'xpack.apm.transactionDetails.spanFlyout.durationLabel',
{
defaultMessage: 'Duration'
}
),
val: asMillis(spanDuration),
width: '50%'
},
{
label: i18n.translate(
'xpack.apm.transactionDetails.spanFlyout.percentOfTransactionLabel',
{
defaultMessage: '% of transaction'
}
),
val: asPercent(spanDuration, totalDuration),
width: '50%'
},
{
fieldName: SPAN_TYPE,
label: i18n.translate(
'xpack.apm.transactionDetails.spanFlyout.typeLabel',
{
defaultMessage: 'Type'
}
),
val: spanType,
truncated: true,
width: '15%'
}
...transactionStickyProperties
];
if (spanSubtype) {
stickyProperties.push({
fieldName: SPAN_SUBTYPE,
label: i18n.translate(
'xpack.apm.transactionDetails.spanFlyout.subtypeLabel',
{
defaultMessage: 'Subtype'
}
),
val: spanSubtype,
truncated: true,
width: '15%'
});
}
if (spanAction) {
stickyProperties.push({
fieldName: SPAN_ACTION,
label: i18n.translate(
'xpack.apm.transactionDetails.spanFlyout.actionLabel',
{
defaultMessage: 'Action'
}
),
val: spanAction,
truncated: true,
width: '15%'
});
}
return <StickyProperties stickyProperties={stickyProperties} />;
}

View file

@ -15,27 +15,72 @@ import {
EuiPortal,
EuiSpacer,
EuiTabbedContent,
EuiTitle
EuiTitle,
EuiBadge,
EuiToolTip
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { get, keys } from 'lodash';
import React, { Fragment } from 'react';
import styled from 'styled-components';
import { idx } from '@kbn/elastic-idx';
import { px, units } from '../../../../../../../style/variables';
import { Summary } from '../../../../../../shared/Summary';
import { TimestampSummaryItem } from '../../../../../../shared/Summary/TimestampSummaryItem';
import { DurationSummaryItem } from '../../../../../../shared/Summary/DurationSummaryItem';
import { Span } from '../../../../../../../../typings/es_schemas/ui/Span';
import { Transaction } from '../../../../../../../../typings/es_schemas/ui/Transaction';
import { DiscoverSpanLink } from '../../../../../../shared/Links/DiscoverLinks/DiscoverSpanLink';
import { Stacktrace } from '../../../../../../shared/Stacktrace';
import { FlyoutTopLevelProperties } from '../FlyoutTopLevelProperties';
import { ResponsiveFlyout } from '../ResponsiveFlyout';
import { DatabaseContext } from './DatabaseContext';
import { HttpContext } from './HttpContext';
import { StickySpanProperties } from './StickySpanProperties';
function formatType(type: string) {
switch (type) {
case 'db':
return 'DB';
case 'hard-navigation':
return i18n.translate(
'xpack.apm.transactionDetails.spanFlyout.spanType.navigationTimingLabel',
{
defaultMessage: 'Navigation timing'
}
);
default:
return type;
}
}
function formatSubtype(subtype: string | undefined) {
switch (subtype) {
case 'mysql':
return 'MySQL';
default:
return subtype;
}
}
function getSpanTypes(span: Span) {
const { type, subtype, action } = span.span;
return {
spanType: formatType(type),
spanSubtype: formatSubtype(subtype),
spanAction: action
};
}
const TagName = styled.div`
font-weight: bold;
`;
const SpanBadge = styled(EuiBadge)`
display: inline-block;
margin-right: ${px(units.quarter)};
`;
interface Props {
span?: Span;
parentTransaction?: Transaction;
@ -62,6 +107,7 @@ export function SpanFlyout({
key,
value: get(spanLabels, key)
}));
const spanTypes = getSpanTypes(span);
return (
<EuiPortal>
@ -96,9 +142,50 @@ export function SpanFlyout({
</EuiFlexGroup>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<FlyoutTopLevelProperties transaction={parentTransaction} />
<EuiHorizontalRule />
<StickySpanProperties span={span} totalDuration={totalDuration} />
<StickySpanProperties span={span} transaction={parentTransaction} />
<EuiSpacer size="m" />
<Summary
items={[
<TimestampSummaryItem time={span.timestamp.us / 1000} />,
<DurationSummaryItem
duration={span.span.duration.us}
totalDuration={totalDuration}
parentType="transaction"
/>,
<>
<EuiToolTip
content={i18n.translate(
'xpack.apm.transactionDetails.spanFlyout.spanType',
{ defaultMessage: 'Type' }
)}
>
<SpanBadge color="hollow">{spanTypes.spanType}</SpanBadge>
</EuiToolTip>
{spanTypes.spanSubtype && (
<EuiToolTip
content={i18n.translate(
'xpack.apm.transactionDetails.spanFlyout.spanSubtype',
{ defaultMessage: 'Subtype' }
)}
>
<SpanBadge color="hollow">
{spanTypes.spanSubtype}
</SpanBadge>
</EuiToolTip>
)}
{spanTypes.spanAction && (
<EuiToolTip
content={i18n.translate(
'xpack.apm.transactionDetails.spanFlyout.spanAction',
{ defaultMessage: 'Action' }
)}
>
<SpanBadge color="hollow">{spanTypes.spanAction}</SpanBadge>
</EuiToolTip>
)}
</>
]}
/>
<EuiHorizontalRule />
<HttpContext httpContext={httpContext} />
<DatabaseContext dbContext={dbContext} />

View file

@ -9,7 +9,6 @@ import {
EuiFlexItem,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiHorizontalRule,
EuiPortal,
EuiSpacer,
EuiTitle
@ -18,7 +17,7 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
import { Transaction } from '../../../../../../../../typings/es_schemas/ui/Transaction';
import { TransactionActionMenu } from '../../../../../../shared/TransactionActionMenu/TransactionActionMenu';
import { StickyTransactionProperties } from '../../../StickyTransactionProperties';
import { TransactionSummary } from '../../../../../../shared/Summary/TransactionSummary';
import { FlyoutTopLevelProperties } from '../FlyoutTopLevelProperties';
import { ResponsiveFlyout } from '../ResponsiveFlyout';
import { TransactionMetadata } from '../../../../../../shared/MetadataTable/TransactionMetadata';
@ -82,13 +81,13 @@ export function TransactionFlyout({
</EuiFlyoutHeader>
<EuiFlyoutBody>
<FlyoutTopLevelProperties transaction={transactionDoc} />
<EuiHorizontalRule />
<StickyTransactionProperties
errorCount={errorCount}
<EuiSpacer size="m" />
<TransactionSummary
transaction={transactionDoc}
totalDuration={traceRootDuration}
errorCount={errorCount}
/>
<EuiHorizontalRule />
<EuiSpacer size="m" />
<DroppedSpansWarning transactionDoc={transactionDoc} />
<TransactionPropertiesTable transaction={transactionDoc} />
</EuiFlyoutBody>

View file

@ -25,7 +25,7 @@ import { TransactionActionMenu } from '../../../shared/TransactionActionMenu/Tra
import { TransactionTabs } from './TransactionTabs';
import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers';
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
import { TraceSummary } from './TraceSummary/TraceSummary';
import { TransactionSummary } from '../../../shared/Summary/TransactionSummary';
function MaybeViewTraceLink({
transaction,
@ -165,7 +165,7 @@ export const WaterfallWithSummmary: React.SFC<Props> = ({
<EuiSpacer size="s" />
<TraceSummary
<TransactionSummary
errorCount={sum(Object.values(waterfall.errorCountByTransactionId))}
totalDuration={waterfall.traceRootDuration}
transaction={entryTransaction}

View file

@ -0,0 +1,46 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { EuiToolTip, EuiText } from '@elastic/eui';
import { PercentOfParent } from '../../app/TransactionDetails/WaterfallWithSummmary/PercentOfParent';
import { asTime } from '../../../utils/formatters';
interface Props {
duration: number;
totalDuration: number | undefined;
parentType: 'trace' | 'transaction';
}
const DurationSummaryItem = ({
duration,
totalDuration,
parentType
}: Props) => {
const calculatedTotalDuration =
totalDuration === undefined ? duration : totalDuration;
const label = i18n.translate('xpack.apm.transactionDurationLabel', {
defaultMessage: 'Duration'
});
return (
<>
<EuiToolTip content={label}>
<EuiText>{asTime(duration)}</EuiText>
</EuiToolTip>
&nbsp;
<EuiText size="s">
<PercentOfParent
duration={duration}
totalDuration={calculatedTotalDuration}
parentType={parentType}
/>
</EuiText>
</>
);
};
export { DurationSummaryItem };

View file

@ -0,0 +1,33 @@
/*
* 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 { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import { px } from '../../../../../code/public/style/variables';
import { ErrorCountBadge } from '../../app/TransactionDetails/WaterfallWithSummmary/ErrorCountBadge';
import { units } from '../../../style/variables';
interface Props {
count: number;
}
const Badge = styled(ErrorCountBadge)`
margin-top: ${px(units.eighth)};
`;
const ErrorCountSummaryItem = ({ count }: Props) => {
return (
<Badge>
{i18n.translate('xpack.apm.transactionDetails.errorCount', {
defaultMessage:
'{errorCount, number} {errorCount, plural, one {Error} other {Errors}}',
values: { errorCount: count }
})}
</Badge>
);
};
export { ErrorCountSummaryItem };

View file

@ -6,24 +6,26 @@
import React from 'react';
import { shallow, mount } from 'enzyme';
import { HttpInfo } from './HttpInfo';
import * as exampleTransactions from './__fixtures__/transactions';
import { palettes } from '@elastic/eui';
import { cloneDeep, set } from 'lodash';
import { HttpInfoSummaryItem } from './';
import * as exampleTransactions from '../__fixtures__/transactions';
describe('HttpInfo', () => {
describe('HttpInfoSummaryItem', () => {
describe('render', () => {
const transaction = exampleTransactions.httpOk;
const url = 'https://example.com';
const props = { transaction, url };
const method = 'get';
const props = { transaction, url, method, status: 200 };
it('renders', () => {
expect(() => shallow(<HttpInfo {...props} />)).not.toThrowError();
expect(() =>
shallow(<HttpInfoSummaryItem {...props} />)
).not.toThrowError();
});
describe('with status code 200', () => {
it('shows a success color', () => {
const wrapper = mount(<HttpInfo {...props} />);
const wrapper = mount(<HttpInfoSummaryItem {...props} />);
expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual(
palettes.euiPaletteForStatus.colors[0]
@ -33,10 +35,9 @@ describe('HttpInfo', () => {
describe('with status code 301', () => {
it('shows a warning color', () => {
const p = cloneDeep(props);
set(p, 'transaction.http.response.status_code', 301);
const p = { ...props, status: 301 };
const wrapper = mount(<HttpInfo {...p} />);
const wrapper = mount(<HttpInfoSummaryItem {...p} />);
expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual(
palettes.euiPaletteForStatus.colors[4]
@ -46,10 +47,9 @@ describe('HttpInfo', () => {
describe('with status code 404', () => {
it('shows a error color', () => {
const p = cloneDeep(props);
set(p, 'transaction.http.response.status_code', 404);
const p = { ...props, status: 404 };
const wrapper = mount(<HttpInfo {...p} />);
const wrapper = mount(<HttpInfoSummaryItem {...p} />);
expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual(
palettes.euiPaletteForStatus.colors[9]
@ -59,10 +59,9 @@ describe('HttpInfo', () => {
describe('with status code 502', () => {
it('shows a error color', () => {
const p = cloneDeep(props);
set(p, 'transaction.http.response.status_code', 502);
const p = { ...props, status: 502 };
const wrapper = mount(<HttpInfo {...p} />);
const wrapper = mount(<HttpInfoSummaryItem {...p} />);
expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual(
palettes.euiPaletteForStatus.colors[9]
@ -72,10 +71,9 @@ describe('HttpInfo', () => {
describe('with some other status code', () => {
it('shows the default color', () => {
const p = cloneDeep(props);
set(p, 'transaction.http.response.status_code', 700);
const p = { ...props, status: 700 };
const wrapper = mount(<HttpInfo {...p} />);
const wrapper = mount(<HttpInfoSummaryItem {...p} />);
expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual(
'default'

View file

@ -8,10 +8,8 @@ import React from 'react';
import { EuiToolTip, EuiBadge } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import { idx } from '@kbn/elastic-idx/target';
import { palettes } from '@elastic/eui';
import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction';
import { units, px, truncate, unit } from '../../../../../style/variables';
import { units, px, truncate, unit } from '../../../../style/variables';
import { statusCodes } from './statusCodes';
const statusColors = {
@ -58,7 +56,8 @@ const Url = styled('span')`
${truncate(px(unit * 24))};
`;
interface HttpInfoProps {
transaction: Transaction;
method: string;
status?: number;
url: string;
}
@ -66,12 +65,7 @@ const Span = styled('span')`
white-space: nowrap;
`;
export function HttpInfo({ transaction, url }: HttpInfoProps) {
const method = (
idx(transaction, _ => _.http.request.method) || ''
).toUpperCase();
const status = idx(transaction, _ => _.http.response.status_code);
export function HttpInfoSummaryItem({ status, method, url }: HttpInfoProps) {
const methodLabel = i18n.translate(
'xpack.apm.transactionDetails.requestMethodLabel',
{
@ -83,7 +77,7 @@ export function HttpInfo({ transaction, url }: HttpInfoProps) {
<Span>
<HttpInfoBadge title={undefined}>
<EuiToolTip content={methodLabel}>
<strong>{method}</strong>
<>{method.toUpperCase()}</>
</EuiToolTip>{' '}
<EuiToolTip content={url}>
<Url>{url}</Url>

View file

@ -0,0 +1,26 @@
/*
* 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 { EuiToolTip } from '@elastic/eui';
import moment from 'moment-timezone';
interface Props {
time: number;
}
const TimestampSummaryItem = (props: Props) => {
const time = moment.tz(props.time, moment.tz.guess());
const relativeTimeLabel = time.fromNow();
const absoluteTimeLabel = time.format('MMM Do YYYY HH:mm:ss.SSS zz');
return (
<EuiToolTip content={absoluteTimeLabel}>
<>{relativeTimeLabel}</>
</EuiToolTip>
);
};
export { TimestampSummaryItem };

View file

@ -8,11 +8,11 @@ import React from 'react';
import { EuiToolTip, EuiBadge } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
interface ResultProps {
result: string;
interface Props {
transactionResult: string;
}
export function Result({ result }: ResultProps) {
export function TransactionResultSummaryItem({ transactionResult }: Props) {
return (
<EuiToolTip
content={i18n.translate('xpack.apm.transactionDetails.resultLabel', {
@ -20,7 +20,7 @@ export function Result({ result }: ResultProps) {
})}
>
<EuiBadge color="default" title={undefined}>
{result}
{transactionResult}
</EuiBadge>
</EuiToolTip>
);

View file

@ -5,12 +5,12 @@
*/
import { shallow } from 'enzyme';
import { TraceSummary } from '.';
import React from 'react';
import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction';
import { TransactionSummary } from './TransactionSummary';
import { Transaction } from '../../../../typings/es_schemas/ui/Transaction';
import * as exampleTransactions from './__fixtures__/transactions';
describe('TraceSummary', () => {
describe('TransactionSummary', () => {
describe('render', () => {
const transaction: Transaction = exampleTransactions.httpOk;
@ -21,7 +21,9 @@ describe('TraceSummary', () => {
};
it('renders', () => {
expect(() => shallow(<TraceSummary {...props} />)).not.toThrowError();
expect(() =>
shallow(<TransactionSummary {...props} />)
).not.toThrowError();
});
});
});

View file

@ -0,0 +1,63 @@
/*
* 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 { idx } from '@kbn/elastic-idx';
import { Transaction } from '../../../../typings/es_schemas/ui/Transaction';
import { Summary } from './';
import { TimestampSummaryItem } from './TimestampSummaryItem';
import { DurationSummaryItem } from './DurationSummaryItem';
import { ErrorCountSummaryItem } from './ErrorCountSummaryItem';
import { isRumAgentName } from '../../../../common/agent_name';
import { HttpInfoSummaryItem } from './HttpInfoSummaryItem';
import { TransactionResultSummaryItem } from './TransactionResultSummaryItem';
interface Props {
transaction: Transaction;
totalDuration: number | undefined;
errorCount: number;
}
const getTransactionResultSummaryItem = (transaction: Transaction) => {
const result = idx(transaction, _ => _.transaction.result);
const isRumAgent = isRumAgentName(transaction.agent.name);
const url = isRumAgent
? idx(transaction, _ => _.transaction.page.url)
: idx(transaction, _ => _.url.full);
if (url) {
const method = idx(transaction, _ => _.http.request.method) || '';
const status = idx(transaction, _ => _.http.response.status_code);
return <HttpInfoSummaryItem method={method} status={status} url={url} />;
}
if (result) {
return <TransactionResultSummaryItem transactionResult={result} />;
}
return null;
};
const TransactionSummary = ({
transaction,
totalDuration,
errorCount
}: Props) => {
const items = [
<TimestampSummaryItem time={transaction.timestamp.us / 1000} />,
<DurationSummaryItem
duration={transaction.transaction.duration.us}
totalDuration={totalDuration}
parentType="trace"
/>,
getTransactionResultSummaryItem(transaction),
errorCount ? <ErrorCountSummaryItem count={errorCount} /> : null
];
return <Summary items={items}></Summary>;
};
export { TransactionSummary };

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Transaction } from '../../../../../../../typings/es_schemas/ui/Transaction';
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
export const httpOk: Transaction = {
'@timestamp': '0',

View file

@ -0,0 +1,46 @@
/*
* 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 { EuiFlexGrid, EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { px, units } from '../../../../public/style/variables';
interface Props {
items: Array<React.ReactElement | null | undefined>;
}
// TODO: Light/Dark theme (@see https://github.com/elastic/kibana/issues/44840)
const theme = euiLightVars;
const Item = styled(EuiFlexItem)`
flex-wrap: nowrap;
border-right: 1px solid ${theme.euiColorLightShade};
padding-right: ${px(units.half)};
flex-flow: row nowrap;
line-height: 1.5;
align-items: center !important;
&:last-child {
border-right: none;
padding-right: 0;
}
`;
const Summary = ({ items }: Props) => {
const filteredItems = items.filter(Boolean) as React.ReactElement[];
return (
<EuiFlexGrid gutterSize="s">
{filteredItems.map((item, index) => (
<Item key={index} grow={false}>
{item}
</Item>
))}
</EuiFlexGrid>
);
};
export { Summary };

View file

@ -3315,17 +3315,12 @@
"xpack.apm.errorGroupDetails.errorGroupTitle": "エラーグループ {errorGroupId}",
"xpack.apm.errorGroupDetails.errorOccurrenceTitle": "エラーのオカレンス",
"xpack.apm.errorGroupDetails.exceptionMessageLabel": "例外メッセージ",
"xpack.apm.errorGroupDetails.handledLabel": "対応済み",
"xpack.apm.errorGroupDetails.logMessageLabel": "ログメッセージ",
"xpack.apm.errorGroupDetails.noErrorsLabel": "エラーが見つかりませんでした",
"xpack.apm.errorGroupDetails.occurrencesChartLabel": "オカレンス",
"xpack.apm.errorGroupDetails.occurrencesLongLabel": "{occCount} 件",
"xpack.apm.errorGroupDetails.occurrencesShortLabel": "{occCount} 件",
"xpack.apm.errorGroupDetails.requestMethodLabel": "リクエストメソド",
"xpack.apm.errorGroupDetails.timestampLabel": "タイムスタンプ",
"xpack.apm.errorGroupDetails.transactionSampleIdLabel": "トランザクションサンプル ID",
"xpack.apm.errorGroupDetails.unhandledLabel": "未対応",
"xpack.apm.errorGroupDetails.userIdLabel": "ユーザー ID",
"xpack.apm.errorGroupDetails.viewOccurrencesInDiscoverButtonLabel": "ディスカバリで {occurrencesCount} 件のオカレンスを表示",
"xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel": "エラーメッセージと原因",
"xpack.apm.errorsTable.groupIdColumnLabel": "グループ ID",
@ -3494,16 +3489,11 @@
"xpack.apm.transactionDetails.resultLabel": "結果",
"xpack.apm.transactionDetails.serviceLabel": "サービス",
"xpack.apm.transactionDetails.servicesTitle": "サービス",
"xpack.apm.transactionDetails.spanFlyout.actionLabel": "アクション",
"xpack.apm.transactionDetails.spanFlyout.databaseStatementTitle": "データベースステートメント",
"xpack.apm.transactionDetails.spanFlyout.durationLabel": "期間",
"xpack.apm.transactionDetails.spanFlyout.nameLabel": "名前",
"xpack.apm.transactionDetails.spanFlyout.percentOfTransactionLabel": "トランザクションの %",
"xpack.apm.transactionDetails.spanFlyout.spanDetailsTitle": "スパン詳細",
"xpack.apm.transactionDetails.spanFlyout.spanType.navigationTimingLabel": "ナビゲーションタイミング",
"xpack.apm.transactionDetails.spanFlyout.stackTraceTabLabel": "スタックトレース",
"xpack.apm.transactionDetails.spanFlyout.subtypeLabel": "サブタイプ",
"xpack.apm.transactionDetails.spanFlyout.typeLabel": "タイプ",
"xpack.apm.transactionDetails.spanFlyout.viewSpanInDiscoverButtonLabel": "ディスカバリでスパンを表示",
"xpack.apm.transactionDetails.timestampLabel": "タイムスタンプ",
"xpack.apm.transactionDetails.transactionLabel": "トランザクション",
@ -11572,4 +11562,4 @@
"xpack.fileUpload.fileParser.noFileProvided": "エラー、ファイルが提供されていません",
"xpack.fileUpload.jsonIndexFilePicker.errorGettingIndexName": "インデックス名の取得中にエラーが発生: {errorMessage}"
}
}
}

View file

@ -3316,17 +3316,12 @@
"xpack.apm.errorGroupDetails.errorGroupTitle": "错误组 {errorGroupId}",
"xpack.apm.errorGroupDetails.errorOccurrenceTitle": "错误发生",
"xpack.apm.errorGroupDetails.exceptionMessageLabel": "异常消息",
"xpack.apm.errorGroupDetails.handledLabel": "已处理",
"xpack.apm.errorGroupDetails.logMessageLabel": "日志消息",
"xpack.apm.errorGroupDetails.noErrorsLabel": "未找到任何错误",
"xpack.apm.errorGroupDetails.occurrencesChartLabel": "发生次数",
"xpack.apm.errorGroupDetails.occurrencesLongLabel": "{occCount} 次发生",
"xpack.apm.errorGroupDetails.occurrencesShortLabel": "{occCount} 次发生",
"xpack.apm.errorGroupDetails.requestMethodLabel": "请求方法",
"xpack.apm.errorGroupDetails.timestampLabel": "时间戳",
"xpack.apm.errorGroupDetails.transactionSampleIdLabel": "事务样例 ID",
"xpack.apm.errorGroupDetails.unhandledLabel": "未处理",
"xpack.apm.errorGroupDetails.userIdLabel": "用户 ID",
"xpack.apm.errorGroupDetails.viewOccurrencesInDiscoverButtonLabel": "在 Discover 中查看 {occurrencesCount} 次发生",
"xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel": "错误消息和原因",
"xpack.apm.errorsTable.groupIdColumnLabel": "组 ID",
@ -3495,16 +3490,11 @@
"xpack.apm.transactionDetails.resultLabel": "结果",
"xpack.apm.transactionDetails.serviceLabel": "服务",
"xpack.apm.transactionDetails.servicesTitle": "服务",
"xpack.apm.transactionDetails.spanFlyout.actionLabel": "操作",
"xpack.apm.transactionDetails.spanFlyout.databaseStatementTitle": "数据库语句",
"xpack.apm.transactionDetails.spanFlyout.durationLabel": "持续时间",
"xpack.apm.transactionDetails.spanFlyout.nameLabel": "名称",
"xpack.apm.transactionDetails.spanFlyout.percentOfTransactionLabel": "事务的 %",
"xpack.apm.transactionDetails.spanFlyout.spanDetailsTitle": "跨度详情",
"xpack.apm.transactionDetails.spanFlyout.spanType.navigationTimingLabel": "导航定时",
"xpack.apm.transactionDetails.spanFlyout.stackTraceTabLabel": "堆栈追溯",
"xpack.apm.transactionDetails.spanFlyout.subtypeLabel": "子类型",
"xpack.apm.transactionDetails.spanFlyout.typeLabel": "类型",
"xpack.apm.transactionDetails.spanFlyout.viewSpanInDiscoverButtonLabel": "在 Discover 中查看跨度",
"xpack.apm.transactionDetails.timestampLabel": "时间戳",
"xpack.apm.transactionDetails.transactionLabel": "事务",
@ -11574,4 +11564,4 @@
"xpack.fileUpload.fileParser.noFileProvided": "错误,未提供任何文件",
"xpack.fileUpload.jsonIndexFilePicker.errorGettingIndexName": "检索索引名称时出错:{errorMessage}"
}
}
}