[APM] Create Stackframe component, tests for Error interface and more TS (#27139)

This commit is contained in:
Søren Louv-Jansen 2018-12-18 14:37:09 +01:00 committed by GitHub
parent e43030885f
commit 5a763ec71b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 2098 additions and 1186 deletions

View file

@ -1,5 +1,65 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Error v2 ERROR_CULPRIT 1`] = `"handleOopsie"`;
exports[`Error v2 ERROR_EXC_HANDLED 1`] = `false`;
exports[`Error v2 ERROR_EXC_MESSAGE 1`] = `"sonic boom"`;
exports[`Error v2 ERROR_EXC_STACKTRACE 1`] = `undefined`;
exports[`Error v2 ERROR_GROUP_ID 1`] = `"grouping key"`;
exports[`Error v2 ERROR_LOG_MESSAGE 1`] = `undefined`;
exports[`Error v2 ERROR_LOG_STACKTRACE 1`] = `undefined`;
exports[`Error v2 PARENT_ID 1`] = `"parentId"`;
exports[`Error v2 PROCESSOR_EVENT 1`] = `"error"`;
exports[`Error v2 PROCESSOR_NAME 1`] = `"error"`;
exports[`Error v2 REQUEST_METHOD 1`] = `undefined`;
exports[`Error v2 REQUEST_URL_FULL 1`] = `undefined`;
exports[`Error v2 SERVICE_AGENT_NAME 1`] = `"agent name"`;
exports[`Error v2 SERVICE_LANGUAGE_NAME 1`] = `"nodejs"`;
exports[`Error v2 SERVICE_NAME 1`] = `"service name"`;
exports[`Error v2 SPAN_DURATION 1`] = `undefined`;
exports[`Error v2 SPAN_HEX_ID 1`] = `undefined`;
exports[`Error v2 SPAN_ID 1`] = `undefined`;
exports[`Error v2 SPAN_NAME 1`] = `undefined`;
exports[`Error v2 SPAN_SQL 1`] = `undefined`;
exports[`Error v2 SPAN_START 1`] = `undefined`;
exports[`Error v2 SPAN_TYPE 1`] = `undefined`;
exports[`Error v2 TRACE_ID 1`] = `"trace id"`;
exports[`Error v2 TRANSACTION_DURATION 1`] = `undefined`;
exports[`Error v2 TRANSACTION_ID 1`] = `"transaction id"`;
exports[`Error v2 TRANSACTION_NAME 1`] = `undefined`;
exports[`Error v2 TRANSACTION_RESULT 1`] = `undefined`;
exports[`Error v2 TRANSACTION_SAMPLED 1`] = `undefined`;
exports[`Error v2 TRANSACTION_TYPE 1`] = `undefined`;
exports[`Error v2 USER_ID 1`] = `undefined`;
exports[`Span v1 ERROR_CULPRIT 1`] = `undefined`;
exports[`Span v1 ERROR_EXC_HANDLED 1`] = `undefined`;

View file

@ -5,6 +5,7 @@
*/
import { get } from 'lodash';
import { APMError } from '../typings/es_schemas/Error';
import { Span } from '../typings/es_schemas/Span';
import { Transaction } from '../typings/es_schemas/Transaction';
import * as constants from './constants';
@ -19,7 +20,7 @@ describe('Transaction v1:', () => {
version: 'beat version'
},
host: {
name: 'string;'
name: 'my hostname'
},
processor: {
name: 'transaction',
@ -77,7 +78,7 @@ describe('Transaction v2', () => {
name: 'beat name',
version: 'beat version'
},
host: { name: 'string;' },
host: { name: 'my hostname' },
processor: { name: 'transaction', event: 'transaction' },
timestamp: { us: 1337 },
trace: { id: 'trace id' },
@ -122,7 +123,7 @@ describe('Span v1', () => {
version: 'beat version'
},
host: {
name: 'string;'
name: 'my hostname'
},
processor: {
name: 'transaction',
@ -173,7 +174,7 @@ describe('Span v2', () => {
version: 'beat version'
},
host: {
name: 'string;'
name: 'my hostname'
},
processor: {
name: 'transaction',
@ -221,7 +222,69 @@ describe('Span v2', () => {
matchSnapshot(span);
});
function matchSnapshot(obj: Span | Transaction) {
describe('Error v2', () => {
const errorDoc: APMError = {
agent: {
hostname: 'agent hostname',
type: 'apm-server',
version: '7.0.0'
},
error: {
exception: {
module: 'errors',
handled: false,
message: 'sonic boom',
type: 'errorString'
},
culprit: 'handleOopsie',
id: 'error id',
grouping_key: 'grouping key'
},
version: 'v2',
'@timestamp': new Date().toString(),
beat: {
hostname: 'beat hostname',
name: 'beat name',
version: 'beat version'
},
host: {
name: 'my hostname'
},
processor: {
name: 'error',
event: 'error'
},
timestamp: {
us: 1337
},
trace: {
id: 'trace id'
},
context: {
service: {
name: 'service name',
agent: {
name: 'agent name',
version: 'v1337'
},
language: {
name: 'nodejs',
version: 'v1337'
}
}
},
parent: {
id: 'parentId'
},
transaction: {
id: 'transaction id'
}
};
matchSnapshot(errorDoc);
});
function matchSnapshot(obj: Span | Transaction | APMError) {
Object.entries(constants).forEach(([key, longKey]) => {
const value = get(obj, longKey);
it(key, () => {

View file

@ -16,7 +16,7 @@ import {
} from '../../../../../../../style/variables';
import { EuiTitle } from '@elastic/eui';
import { HttpContext } from '../../../../../../../../typings/es_schemas/Span';
import { Span } from 'x-pack/plugins/apm/typings/es_schemas/Span';
const DatabaseStatement = styled.div`
margin-top: ${px(unit)};
@ -28,7 +28,7 @@ const DatabaseStatement = styled.div`
`;
interface Props {
httpContext?: HttpContext;
httpContext: Span['context']['http'];
}
export function HttpContext({ httpContext }: Props) {

View file

@ -25,14 +25,12 @@ import styled from 'styled-components';
import { SERVICE_LANGUAGE_NAME } from '../../../../../../../../common/constants';
import { px, unit } from '../../../../../../../style/variables';
// @ts-ignore
import { Stacktrace } from '../../../../../../shared/Stacktrace';
import { DatabaseContext } from './DatabaseContext';
import { HttpContext } from './HttpContext';
import { StickySpanProperties } from './StickySpanProperties';
import { DiscoverSpanButton } from 'x-pack/plugins/apm/public/components/shared/DiscoverButtons/DiscoverSpanButton';
import { Stacktrace } from 'x-pack/plugins/apm/public/components/shared/Stacktrace';
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
import { Span } from '../../../../../../../../typings/es_schemas/Span';
import { FlyoutTopLevelProperties } from '../FlyoutTopLevelProperties';

View file

@ -1,53 +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 styled from 'styled-components';
import {
colors,
borderRadius,
px,
units,
fontFamily,
unit
} from '../../../style/variables';
import { Ellipsis } from '../../shared/Icons';
import { PropertiesTable } from '../../shared/PropertiesTable';
const VariablesContainer = styled.div`
background: ${colors.white};
border-top: 1px solid ${colors.gray4};
border-radius: 0 0 ${borderRadius} ${borderRadius};
padding: ${px(units.half)} ${px(unit)};
font-family: ${fontFamily};
`;
const VariablesToggle = styled.a`
display: block;
cursor: pointer;
user-select: none;
`;
const VariablesTableContainer = styled.div`
padding: ${px(units.plus)} ${px(unit)} 0;
`;
export function Variables({ vars, visible, onClick }) {
return (
<VariablesContainer>
<VariablesToggle onClick={onClick}>
<Ellipsis horizontal={visible} style={{ marginRight: units.half }} />{' '}
Local variables
</VariablesToggle>
{visible && (
<VariablesTableContainer>
<PropertiesTable propData={vars} propKey={'custom'} />
</VariablesTableContainer>
)}
</VariablesContainer>
);
}

View file

@ -1,19 +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 { mount } from 'enzyme';
import { CodePreview } from '../index';
import props from './props.json';
import { toJson } from '../../../../utils/testHelpers';
describe('CodePreview', () => {
it('should render with data', () => {
const wrapper = mount(<CodePreview {...props} />);
expect(toJson(wrapper)).toMatchSnapshot();
});
});

View file

@ -1,399 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CodePreview should render with data 1`] = `
.c2 {
color: #999999;
padding: 8px;
font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace;
}
.c3 {
font-weight: bold;
color: #000000;
}
.c4 {
position: relative;
border-radius: 0 0 5px 5px;
}
.c5 {
position: absolute;
width: 100%;
height: 18px;
top: 36px;
pointer-events: none;
background-color: #FCF2E6;
}
.c6 {
position: absolute;
top: 0;
left: 0;
border-radius: 0 0 0 5px;
background: #f5f5f5;
}
.c7 {
position: relative;
min-width: 42px;
padding-left: 8px;
padding-right: 4px;
color: #999999;
line-height: 18px;
text-align: right;
border-right: 1px solid #d9d9d9;
}
.c7:last-of-type {
border-radius: 0 0 0 5px;
}
.c8 {
position: relative;
min-width: 42px;
padding-left: 8px;
padding-right: 4px;
color: #999999;
line-height: 18px;
text-align: right;
border-right: 1px solid #d9d9d9;
background-color: #FCF2E6;
}
.c8:last-of-type {
border-radius: 0 0 0 5px;
}
.c9 {
overflow: auto;
margin: 0 0 0 42px;
padding: 0;
background-color: #ffffff;
}
.c9:last-of-type {
border-radius: 0 0 5px 0;
}
.c10 {
margin: 0;
color: inherit;
background: inherit;
border: 0;
border-radius: 0;
overflow: initial;
padding: 0 18px;
line-height: 18px;
}
.c11 {
position: relative;
padding: 0;
margin: 0;
white-space: pre;
z-index: 2;
}
.c1 {
border-bottom: 1px solid #d9d9d9;
border-radius: 5px 5px 0 0;
}
.c0 {
margin: 0 0 24px 0;
position: relative;
font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace;
border: 1px solid #d9d9d9;
border-radius: 5px;
background: #f5f5f5;
}
<div
className="c0"
>
<div
className="c1"
>
<div
className="c2"
>
<span
className="c3"
>
server/coffee.js
</span>
in
<span
className="c3"
>
&lt;anonymous&gt;
</span>
at
<span
className="c3"
>
line
17
</span>
</div>
</div>
<div
className="c4"
>
<div
className="c5"
/>
<div
className="c6"
>
<div
className="c7"
>
15
.
</div>
<div
className="c7"
>
16
.
</div>
<div
className="c8"
>
17
.
</div>
<div
className="c7"
>
18
.
</div>
<div
className="c7"
>
19
.
</div>
</div>
<div
className="c9"
>
<pre
className="c10"
style={
Object {
"background": "#fff",
"color": "black",
"display": "block",
"overflowX": null,
"padding": null,
}
}
>
<code
className="c11"
>
</code>
</pre>
<pre
className="c10"
style={
Object {
"background": "#fff",
"color": "black",
"display": "block",
"overflowX": null,
"padding": null,
}
}
>
<code
className="c11"
>
app.get(
<span
style={
Object {
"color": "#c41a16",
}
}
>
'/log-error'
</span>
,
<span
style={Object {}}
>
<span
style={
Object {
"color": "#aa0d91",
}
}
>
function
</span>
(
<span
style={
Object {
"color": "#5c2699",
}
}
>
req, res
</span>
)
</span>
{
</code>
</pre>
<pre
className="c10"
style={
Object {
"background": "#fff",
"color": "black",
"display": "block",
"overflowX": null,
"padding": null,
}
}
>
<code
className="c11"
>
apm.captureError(
<span
style={
Object {
"color": "#aa0d91",
}
}
>
new
</span>
<span
style={
Object {
"color": "#5c2699",
}
}
>
Error
</span>
(
<span
style={
Object {
"color": "#c41a16",
}
}
>
'foo'
</span>
),
<span
style={Object {}}
>
<span
style={
Object {
"color": "#aa0d91",
}
}
>
function
</span>
(
<span
style={
Object {
"color": "#5c2699",
}
}
>
err
</span>
)
</span>
{
</code>
</pre>
<pre
className="c10"
style={
Object {
"background": "#fff",
"color": "black",
"display": "block",
"overflowX": null,
"padding": null,
}
}
>
<code
className="c11"
>
<span
style={
Object {
"color": "#aa0d91",
}
}
>
if
</span>
(err) {
</code>
</pre>
<pre
className="c10"
style={
Object {
"background": "#fff",
"color": "black",
"display": "block",
"overflowX": null,
"padding": null,
}
}
>
<code
className="c11"
>
res.status(
<span
style={
Object {
"color": "#1c00cf",
}
}
>
500
</span>
).send(
<span
style={
Object {
"color": "#c41a16",
}
}
>
'could not capture error: '
</span>
+ err.message)
</code>
</pre>
</div>
</div>
</div>
`;

View file

@ -1,21 +0,0 @@
{
"stackframe": {
"function": "<anonymous>",
"libraryFrame": false,
"excludeFromGrouping": false,
"context": {
"pre": ["", "app.get('/log-error', function (req, res) {"],
"post": [
" if (err) {",
" res.status(500).send('could not capture error: ' + err.message)"
]
},
"line": {
"number": 17,
"context": " apm.captureError(new Error('foo'), function (err) {"
},
"filename": "server/coffee.js",
"absPath": "/app/server/coffee.js"
},
"codeLanguage": "javascript"
}

View file

@ -1,102 +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, { PureComponent } from 'react';
import styled from 'styled-components';
import {
borderRadius,
colors,
fontFamilyCode,
px,
units
} from '../../../style/variables';
import { isEmpty } from 'lodash';
// TODO add dependency for @types/react-syntax-highlighter
// @ts-ignore
import javascript from 'react-syntax-highlighter/dist/languages/javascript';
// @ts-ignore
import python from 'react-syntax-highlighter/dist/languages/python';
// @ts-ignore
import ruby from 'react-syntax-highlighter/dist/languages/ruby';
// @ts-ignore
import { registerLanguage } from 'react-syntax-highlighter/dist/light';
import { Stackframe } from '../../../../typings/es_schemas/APMDoc';
import { FrameHeading } from '../Stacktrace/FrameHeading';
// @ts-ignore
import { Context } from './Context';
// @ts-ignore
import { Variables } from './Variables';
registerLanguage('javascript', javascript);
registerLanguage('python', python);
registerLanguage('ruby', ruby);
const CodeHeader = styled.div`
border-bottom: 1px solid ${colors.gray4};
border-radius: ${borderRadius} ${borderRadius} 0 0;
`;
interface ContainerProps {
isLibraryFrame?: boolean;
}
const Container = styled.div<ContainerProps>`
margin: 0 0 ${px(units.plus)} 0;
position: relative;
font-family: ${fontFamilyCode};
border: 1px solid ${colors.gray4};
border-radius: ${borderRadius};
background: ${props => (props.isLibraryFrame ? colors.white : colors.gray5)};
`;
interface Props {
isLibraryFrame?: boolean;
codeLanguage?: string;
stackframe: Stackframe;
}
export class CodePreview extends PureComponent<Props> {
public state = {
variablesVisible: false
};
public toggleVariables = () =>
this.setState(() => {
return { variablesVisible: !this.state.variablesVisible };
});
public render() {
const { stackframe, codeLanguage, isLibraryFrame } = this.props;
const hasVariables = !isEmpty(stackframe.vars);
return (
<Container isLibraryFrame={isLibraryFrame}>
<CodeHeader>
<FrameHeading
stackframe={stackframe}
isLibraryFrame={isLibraryFrame}
/>
</CodeHeader>
<Context
stackframe={stackframe}
codeLanguage={codeLanguage}
isLibraryFrame={isLibraryFrame}
/>
{hasVariables && (
<Variables
vars={stackframe.vars}
visible={this.state.variablesVisible}
onClick={this.toggleVariables}
/>
)}
</Container>
);
}
}

View file

@ -4,14 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { shallow, mount } from 'enzyme';
import { mount, shallow } from 'enzyme';
import 'jest-styled-components';
import React from 'react';
import {
NestedKeyValueTable,
NestedValue,
FormattedKey,
FormattedValue,
FormattedKey
NestedKeyValueTable,
NestedValue
} from '../NestedKeyValueTable';
describe('NestedKeyValueTable component', () => {
@ -30,7 +30,7 @@ describe('NestedKeyValueTable component', () => {
});
describe('NestedValue component', () => {
let props;
let props: any;
beforeEach(() => {
props = {
@ -84,18 +84,16 @@ describe('FormattedValue component', () => {
});
it('should render undefined', () => {
let b;
expect(mount(<FormattedValue value={b} />)).toMatchSnapshot();
expect(mount(<FormattedValue />)).toMatchSnapshot();
expect(mount(<FormattedValue value={undefined} />)).toMatchSnapshot();
});
});
describe('FormattedKey component', () => {
it('should render when the value is null or undefined', () => {
let nope;
expect(mount(<FormattedKey k="testKey" value={null} />)).toMatchSnapshot();
expect(mount(<FormattedKey k="testKey" value={nope} />)).toMatchSnapshot();
expect(mount(<FormattedKey k="testKey" />)).toMatchSnapshot();
expect(
mount(<FormattedKey k="testKey" value={undefined} />)
).toMatchSnapshot();
});
it('should render when the value is defined', () => {

View file

@ -1,137 +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 { shallow } from 'enzyme';
import {
PropertiesTable,
AgentFeatureTipMessage,
sortKeysByConfig,
getPropertyTabNames
} from '..';
import { getAgentFeatureDocsUrl } from '../../../../utils/documentation/agents';
jest.mock('../../../../utils/documentation/agents');
jest.mock('../propertyConfig.json', () => [
{
key: 'testProperty',
required: false,
presortedKeys: ['name', 'age']
},
{
key: 'optionalProperty',
required: false
},
{
key: 'requiredProperty',
required: true
}
]);
describe('PropertiesTable component', () => {
it('should render with data', () => {
expect(
shallow(
<PropertiesTable
propData={{ a: 'hello', b: 'bananas' }}
propKey="testPropKey"
agentName="testAgentName"
/>
)
).toMatchSnapshot();
});
it("should render empty when data isn't present", () => {
expect(
shallow(
<PropertiesTable propKey="testPropKey" agentName="testAgentName" />
)
).toMatchSnapshot();
});
it('should render empty when data has no keys', () => {
expect(
shallow(
<PropertiesTable
propData={{}}
propKey="testPropKey"
agentName="testAgentName"
/>
)
).toMatchSnapshot();
});
});
describe('sortKeysByConfig', () => {
const testData = {
color: 'blue',
name: 'Jess',
age: '39',
numbers: [1, 2, 3],
_id: '44x099z'
};
it('should sort with presorted keys first', () => {
expect(sortKeysByConfig(testData, 'testProperty')).toEqual([
'name',
'age',
'_id',
'color',
'numbers'
]);
});
it('should alpha-sort keys when there is no config value found', () => {
expect(sortKeysByConfig(testData, 'nonExistentKey')).toEqual([
'_id',
'age',
'color',
'name',
'numbers'
]);
});
});
describe('getPropertyTabNames', () => {
it('should return selected and required keys only', () => {
expect(getPropertyTabNames(['testProperty'])).toEqual([
'testProperty',
'requiredProperty'
]);
});
});
describe('AgentFeatureTipMessage component', () => {
const featureName = 'user';
const agentName = 'nodejs';
it('should render when docs are returned', () => {
const mockDocs = 'mock-url';
getAgentFeatureDocsUrl.mockImplementation(() => mockDocs);
expect(
shallow(
<AgentFeatureTipMessage
featureName={featureName}
agentName={agentName}
/>
)
).toMatchSnapshot();
expect(getAgentFeatureDocsUrl).toHaveBeenCalledWith(featureName, agentName);
});
it('should render null empty string when no docs are returned', () => {
getAgentFeatureDocsUrl.mockImplementation(() => null);
expect(
shallow(
<AgentFeatureTipMessage
featureName={featureName}
agentName={agentName}
/>
)
).toMatchSnapshot();
});
});

View file

@ -0,0 +1,163 @@
/*
* 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 } from 'enzyme';
import React from 'react';
import {
AgentFeatureTipMessage,
getPropertyTabNames,
PropertiesTable,
sortKeysByConfig
} from '..';
import * as agentDocs from '../../../../utils/documentation/agents';
import * as propertyConfig from '../propertyConfig';
describe('PropertiesTable', () => {
beforeEach(() => {
mockPropertyConfig();
});
afterEach(() => {
unMockPropertyConfig();
});
describe('PropertiesTable component', () => {
it('should render with data', () => {
expect(
shallow(
<PropertiesTable
propData={{ a: 'hello', b: 'bananas' }}
propKey="testPropKey"
agentName="testAgentName"
/>
)
).toMatchSnapshot();
});
it("should render empty when data isn't present", () => {
expect(
shallow(
<PropertiesTable propKey="testPropKey" agentName="testAgentName" />
)
).toMatchSnapshot();
});
it('should still render NestedKeyValueTable even when data has no keys', () => {
expect(
shallow(
<PropertiesTable
propData={{}}
propKey="testPropKey"
agentName="testAgentName"
/>
)
).toMatchSnapshot();
});
});
describe('sortKeysByConfig', () => {
const testData = {
color: 'blue',
name: 'Jess',
age: '39',
numbers: [1, 2, 3],
_id: '44x099z'
};
it('should sort with presorted keys first', () => {
expect(sortKeysByConfig(testData, 'testProperty')).toEqual([
'name',
'age',
'_id',
'color',
'numbers'
]);
});
it('should alpha-sort keys when there is no config value found', () => {
expect(sortKeysByConfig(testData, 'nonExistentKey')).toEqual([
'_id',
'age',
'color',
'name',
'numbers'
]);
});
});
describe('getPropertyTabNames', () => {
it('should return selected and required keys only', () => {
expect(getPropertyTabNames(['testProperty'])).toEqual([
'testProperty',
'requiredProperty'
]);
});
});
describe('AgentFeatureTipMessage component', () => {
const featureName = 'user';
const agentName = 'nodejs';
it('should render when docs are returned', () => {
jest
.spyOn(agentDocs, 'getAgentFeatureDocsUrl')
.mockImplementation(() => 'mock-url');
expect(
shallow(
<AgentFeatureTipMessage
featureName={featureName}
agentName={agentName}
/>
)
).toMatchSnapshot();
expect(agentDocs.getAgentFeatureDocsUrl).toHaveBeenCalledWith(
featureName,
agentName
);
});
it('should render null empty string when no docs are returned', () => {
jest
.spyOn(agentDocs, 'getAgentFeatureDocsUrl')
.mockImplementation(() => null);
expect(
shallow(
<AgentFeatureTipMessage
featureName={featureName}
agentName={agentName}
/>
)
).toMatchSnapshot();
});
});
});
function mockPropertyConfig() {
// @ts-ignore
propertyConfig.PROPERTY_CONFIG = [
{
key: 'testProperty',
required: false,
presortedKeys: ['name', 'age']
},
{
key: 'optionalProperty',
required: false
},
{
key: 'requiredProperty',
required: true
}
];
}
const originalPropertyConfig = propertyConfig.PROPERTY_CONFIG;
function unMockPropertyConfig() {
// @ts-ignore
propertyConfig.PROPERTY_CONFIG = originalPropertyConfig;
}

View file

@ -64,24 +64,6 @@ exports[`FormattedKey component should render when the value is null or undefine
</FormattedKey>
`;
exports[`FormattedKey component should render when the value is null or undefined 3`] = `
.c0 {
color: #999999;
}
<FormattedKey
k="testKey"
>
<styled.span>
<span
className="c0"
>
testKey
</span>
</styled.span>
</FormattedKey>
`;
exports[`FormattedValue component should render a boolean 1`] = `
<FormattedValue
value={true}
@ -184,22 +166,6 @@ exports[`FormattedValue component should render undefined 1`] = `
</FormattedValue>
`;
exports[`FormattedValue component should render undefined 2`] = `
.c0 {
color: #999999;
}
<FormattedValue>
<styled.span>
<span
className="c0"
>
N/A
</span>
</styled.span>
</FormattedValue>
`;
exports[`NestedKeyValueTable component should render an empty table if there is no data 1`] = `
<styled.table>
<tbody />

View file

@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AgentFeatureTipMessage component should render null empty string when no docs are returned 1`] = `""`;
exports[`PropertiesTable AgentFeatureTipMessage component should render null empty string when no docs are returned 1`] = `""`;
exports[`AgentFeatureTipMessage component should render when docs are returned 1`] = `
exports[`PropertiesTable AgentFeatureTipMessage component should render when docs are returned 1`] = `
<styled.div>
<Styled(EuiIcon)
type="iInCircle"
@ -17,7 +17,7 @@ exports[`AgentFeatureTipMessage component should render when docs are returned 1
</styled.div>
`;
exports[`PropertiesTable component should render empty when data has no keys 1`] = `
exports[`PropertiesTable PropertiesTable component should render empty when data isn't present 1`] = `
<styled.div>
<Styled(styled.div)>
No data available
@ -29,19 +29,7 @@ exports[`PropertiesTable component should render empty when data has no keys 1`]
</styled.div>
`;
exports[`PropertiesTable component should render empty when data isn't present 1`] = `
<styled.div>
<Styled(styled.div)>
No data available
</Styled(styled.div)>
<AgentFeatureTipMessage
agentName="testAgentName"
featureName="testPropKey"
/>
</styled.div>
`;
exports[`PropertiesTable component should render with data 1`] = `
exports[`PropertiesTable PropertiesTable component should render with data 1`] = `
<styled.div>
<NestedKeyValueTable
data={
@ -60,3 +48,18 @@ exports[`PropertiesTable component should render with data 1`] = `
/>
</styled.div>
`;
exports[`PropertiesTable PropertiesTable component should still render NestedKeyValueTable even when data has no keys 1`] = `
<styled.div>
<NestedKeyValueTable
data={Object {}}
depth={1}
keySorter={[Function]}
parentKey="testPropKey"
/>
<AgentFeatureTipMessage
agentName="testAgentName"
featureName="testPropKey"
/>
</styled.div>
`;

View file

@ -8,7 +8,6 @@ import { EuiIcon } from '@elastic/eui';
import _ from 'lodash';
import React from 'react';
import styled from 'styled-components';
import { StringMap } from '../../../../typings/common';
import {
colors,
@ -19,12 +18,9 @@ import {
units
} from '../../../style/variables';
import { getAgentFeatureDocsUrl } from '../../../utils/documentation/agents';
// @ts-ignore
import { ExternalLink } from '../../../utils/url';
import { KeySorter, NestedKeyValueTable } from './NestedKeyValueTable';
import PROPERTY_CONFIG from './propertyConfig.json';
const indexedPropertyConfig = _.indexBy(PROPERTY_CONFIG, 'key');
import { PROPERTY_CONFIG } from './propertyConfig';
const TableContainer = styled.div`
padding-bottom: ${px(units.double)};
@ -89,6 +85,7 @@ export function AgentFeatureTipMessage({
}
export const sortKeysByConfig: KeySorter = (object, currentKey) => {
const indexedPropertyConfig = _.indexBy(PROPERTY_CONFIG, 'key');
const presorted = _.get(
indexedPropertyConfig,
`${currentKey}.presortedKeys`,
@ -102,15 +99,13 @@ export function PropertiesTable({
propKey,
agentName
}: {
propData: StringMap<any>;
propData?: StringMap<any>;
propKey: string;
agentName?: string;
}) {
const hasPropData = !_.isEmpty(propData);
return (
<TableContainer>
{hasPropData ? (
{propData ? (
<NestedKeyValueTable
data={propData}
parentKey={propKey}

View file

@ -1,49 +0,0 @@
[
{
"key": "request",
"required": false,
"presortedKeys": [
"http_version",
"method",
"url",
"socket",
"headers",
"body"
]
},
{
"key": "response",
"required": false,
"presortedKeys": ["status_code", "headers", "headers_sent", "finished"]
},
{
"key": "system",
"required": false,
"presortedKeys": ["hostname", "architecture", "platform"]
},
{
"key": "service",
"required": false,
"presortedKeys": ["runtime", "framework", "agent", "version"]
},
{
"key": "process",
"required": false,
"presortedKeys": ["pid", "title", "argv"]
},
{
"key": "user",
"required": true,
"presortedKeys": ["id", "username", "email"]
},
{
"key": "tags",
"required": true,
"presortedKeys": []
},
{
"key": "custom",
"required": true,
"presortedKeys": []
}
]

View file

@ -0,0 +1,55 @@
/*
* 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 const PROPERTY_CONFIG = [
{
key: 'request',
required: false,
presortedKeys: [
'http_version',
'method',
'url',
'socket',
'headers',
'body'
]
},
{
key: 'response',
required: false,
presortedKeys: ['status_code', 'headers', 'headers_sent', 'finished']
},
{
key: 'system',
required: false,
presortedKeys: ['hostname', 'architecture', 'platform']
},
{
key: 'service',
required: false,
presortedKeys: ['runtime', 'framework', 'agent', 'version']
},
{
key: 'process',
required: false,
presortedKeys: ['pid', 'title', 'argv']
},
{
key: 'user',
required: true,
presortedKeys: ['id', 'username', 'email']
},
{
key: 'tags',
required: true,
presortedKeys: []
},
{
key: 'custom',
required: true,
presortedKeys: []
}
];

View file

@ -4,18 +4,35 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import styled from 'styled-components';
import SyntaxHighlighter from 'react-syntax-highlighter/dist/light';
import { xcode } from 'react-syntax-highlighter/dist/styles';
import { get, size } from 'lodash';
import React from 'react';
// TODO add dependency for @types/react-syntax-highlighter
// @ts-ignore
import javascript from 'react-syntax-highlighter/dist/languages/javascript';
// @ts-ignore
import python from 'react-syntax-highlighter/dist/languages/python';
// @ts-ignore
import ruby from 'react-syntax-highlighter/dist/languages/ruby';
// @ts-ignore
import SyntaxHighlighter from 'react-syntax-highlighter/dist/light';
// @ts-ignore
import { registerLanguage } from 'react-syntax-highlighter/dist/light';
// @ts-ignore
import { xcode } from 'react-syntax-highlighter/dist/styles';
registerLanguage('javascript', javascript);
registerLanguage('python', python);
registerLanguage('ruby', ruby);
import styled from 'styled-components';
import { IStackframeWithLineContext } from 'x-pack/plugins/apm/typings/es_schemas/Stackframe';
import {
unit,
units,
px,
borderRadius,
colors,
borderRadius
px,
unit,
units
} from '../../../style/variables';
const ContextContainer = styled.div`
@ -24,7 +41,7 @@ const ContextContainer = styled.div`
`;
const LINE_HEIGHT = units.eighth * 9;
const LineHighlight = styled.div`
const LineHighlight = styled.div<{ lineNumber: number }>`
position: absolute;
width: 100%;
height: ${px(units.eighth * 9)};
@ -33,7 +50,7 @@ const LineHighlight = styled.div`
background-color: ${colors.yellow};
`;
const LineNumberContainer = styled.div`
const LineNumberContainer = styled.div<{ isLibraryFrame: boolean }>`
position: absolute;
top: 0;
left: 0;
@ -41,7 +58,7 @@ const LineNumberContainer = styled.div`
background: ${props => (props.isLibraryFrame ? colors.white : colors.gray5)};
`;
const LineNumber = styled.div`
const LineNumber = styled.div<{ highlight: boolean }>`
position: relative;
min-width: ${px(units.eighth * 21)};
padding-left: ${px(units.half)};
@ -88,18 +105,25 @@ const Code = styled.code`
z-index: 2;
`;
const getStackframeLines = stackframe => {
const preContext = get(stackframe, 'context.pre', []);
const postContext = get(stackframe, 'context.post', []);
return [...preContext, stackframe.line.context, ...postContext];
};
function getStackframeLines(stackframe: IStackframeWithLineContext) {
const line = stackframe.line.context;
const preLines: string[] = get(stackframe, 'context.pre', []);
const postLines: string[] = get(stackframe, 'context.post', []);
return [...preLines, line, ...postLines];
}
const getStartLineNumber = stackframe => {
function getStartLineNumber(stackframe: IStackframeWithLineContext) {
const preLines = size(get(stackframe, 'context.pre', []));
return stackframe.line.number - preLines;
};
}
export function Context({ stackframe, codeLanguage, isLibraryFrame }) {
interface Props {
stackframe: IStackframeWithLineContext;
codeLanguage?: string;
isLibraryFrame: boolean;
}
export function Context({ stackframe, codeLanguage, isLibraryFrame }: Props) {
const lines = getStackframeLines(stackframe);
const startLineNumber = getStartLineNumber(stackframe);
const highlightedLineIndex = size(get(stackframe, 'context.pre', []));

View file

@ -7,7 +7,7 @@
import { get } from 'lodash';
import React, { Fragment } from 'react';
import styled from 'styled-components';
import { Stackframe } from '../../../../typings/es_schemas/APMDoc';
import { IStackframe } from '../../../../typings/es_schemas/Stackframe';
import { colors, fontFamilyCode, px, units } from '../../../style/variables';
const FileDetails = styled.div`
@ -24,14 +24,11 @@ const AppFrameFileDetail = styled.span`
`;
interface Props {
stackframe: Stackframe;
isLibraryFrame?: boolean;
stackframe: IStackframe;
isLibraryFrame: boolean;
}
const FrameHeading: React.SFC<Props> = ({
stackframe,
isLibraryFrame = false
}) => {
const FrameHeading: React.SFC<Props> = ({ stackframe, isLibraryFrame }) => {
const FileDetail = isLibraryFrame
? LibraryFrameFileDetail
: AppFrameFileDetail;

View file

@ -7,84 +7,77 @@
import { EuiLink } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
import { Stackframe } from 'x-pack/plugins/apm/typings/es_schemas/APMDoc';
import { IStackframe } from 'x-pack/plugins/apm/typings/es_schemas/Stackframe';
import { px, units } from '../../../style/variables';
import { CodePreview } from '../../shared/CodePreview';
// @ts-ignore
import { Ellipsis } from '../../shared/Icons';
import { FrameHeading } from './FrameHeading';
import { hasSourceLines } from './stacktraceUtils';
import { Stackframe } from './Stackframe';
const LibraryFrameToggle = styled.div`
margin: 0 0 ${px(units.plus)} 0;
user-select: none;
`;
interface LibraryStackFrameProps {
codeLanguage?: string;
stackframe: Stackframe;
}
const LibraryStackFrame: React.SFC<LibraryStackFrameProps> = ({
codeLanguage,
stackframe
}) => {
return hasSourceLines(stackframe) ? (
<CodePreview
stackframe={stackframe}
isLibraryFrame
codeLanguage={codeLanguage}
/>
) : (
<FrameHeading stackframe={stackframe} isLibraryFrame />
);
};
interface Props {
visible?: boolean;
stackframes: Stackframe[];
stackframes: IStackframe[];
codeLanguage?: string;
onClick: () => void;
initialVisiblity: boolean;
}
export const LibraryStackFrames: React.SFC<Props> = ({
visible,
stackframes,
codeLanguage,
onClick
}) => {
if (stackframes.length === 0) {
return null;
}
interface State {
isVisible: boolean;
}
export class LibraryStackFrames extends React.Component<Props, State> {
public state = {
isVisible: this.props.initialVisiblity
};
public onClick = () => {
this.setState(({ isVisible }) => ({ isVisible: !isVisible }));
};
public render() {
const { stackframes, codeLanguage } = this.props;
const { isVisible } = this.state;
if (stackframes.length === 0) {
return null;
}
if (stackframes.length === 1) {
return (
<Stackframe
isLibraryFrame
codeLanguage={codeLanguage}
stackframe={stackframes[0]}
/>
);
}
if (stackframes.length === 1) {
return (
<LibraryStackFrame
codeLanguage={codeLanguage}
stackframe={stackframes[0]}
/>
<div>
<LibraryFrameToggle>
<EuiLink onClick={this.onClick}>
<Ellipsis
horizontal={isVisible}
style={{ marginRight: units.half }}
/>{' '}
{stackframes.length} library frames
</EuiLink>
</LibraryFrameToggle>
<div>
{isVisible &&
stackframes.map((stackframe, i) => (
<Stackframe
key={i}
isLibraryFrame
codeLanguage={codeLanguage}
stackframe={stackframe}
/>
))}
</div>
</div>
);
}
return (
<div>
<LibraryFrameToggle>
<EuiLink onClick={onClick}>
<Ellipsis horizontal={visible} style={{ marginRight: units.half }} />{' '}
{stackframes.length} library frames
</EuiLink>
</LibraryFrameToggle>
<div>
{visible &&
stackframes.map((stackframe, i) => (
<LibraryStackFrame
key={i}
codeLanguage={codeLanguage}
stackframe={stackframe}
/>
))}
</div>
</div>
);
};
}

View file

@ -0,0 +1,76 @@
/*
* 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 styled from 'styled-components';
import {
IStackframe,
IStackframeWithLineContext
} from '../../../../typings/es_schemas/Stackframe';
import {
borderRadius,
colors,
fontFamilyCode,
px,
units
} from '../../../style/variables';
import { FrameHeading } from '../Stacktrace/FrameHeading';
import { Context } from './Context';
import { Variables } from './Variables';
const CodeHeader = styled.div`
border-bottom: 1px solid ${colors.gray4};
border-radius: ${borderRadius} ${borderRadius} 0 0;
`;
const Container = styled.div<{ isLibraryFrame: boolean }>`
margin: 0 0 ${px(units.plus)} 0;
position: relative;
font-family: ${fontFamilyCode};
border: 1px solid ${colors.gray4};
border-radius: ${borderRadius};
background: ${props => (props.isLibraryFrame ? colors.white : colors.gray5)};
`;
interface Props {
stackframe: IStackframe;
codeLanguage?: string;
isLibraryFrame?: boolean;
}
export function Stackframe({
stackframe,
codeLanguage,
isLibraryFrame = false
}: Props) {
if (!hasLineContext(stackframe)) {
return (
<FrameHeading stackframe={stackframe} isLibraryFrame={isLibraryFrame} />
);
}
return (
<Container isLibraryFrame={isLibraryFrame}>
<CodeHeader>
<FrameHeading stackframe={stackframe} isLibraryFrame={isLibraryFrame} />
</CodeHeader>
<Context
stackframe={stackframe}
codeLanguage={codeLanguage}
isLibraryFrame={isLibraryFrame}
/>
<Variables vars={stackframe.vars} />
</Container>
);
}
function hasLineContext(
stackframe: IStackframe
): stackframe is IStackframeWithLineContext {
return stackframe.line.context != null;
}

View file

@ -0,0 +1,75 @@
/*
* 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 styled from 'styled-components';
import { IStackframe } from 'x-pack/plugins/apm/typings/es_schemas/Stackframe';
import {
borderRadius,
colors,
fontFamily,
px,
unit,
units
} from '../../../style/variables';
// @ts-ignore
import { Ellipsis } from '../Icons';
import { PropertiesTable } from '../PropertiesTable';
const VariablesContainer = styled.div`
background: ${colors.white};
border-top: 1px solid ${colors.gray4};
border-radius: 0 0 ${borderRadius} ${borderRadius};
padding: ${px(units.half)} ${px(unit)};
font-family: ${fontFamily};
`;
const VariablesToggle = styled.a`
display: block;
cursor: pointer;
user-select: none;
`;
const VariablesTableContainer = styled.div`
padding: ${px(units.plus)} ${px(unit)} 0;
`;
interface Props {
vars: IStackframe['vars'];
}
export class Variables extends React.Component<Props> {
public state = {
isVisible: false
};
public onClick = () => {
this.setState(() => ({ isVisible: !this.state.isVisible }));
};
public render() {
if (!this.props.vars) {
return null;
}
return (
<VariablesContainer>
<VariablesToggle onClick={this.onClick}>
<Ellipsis
horizontal={this.state.isVisible}
style={{ marginRight: units.half }}
/>{' '}
Local variables
</VariablesToggle>
{this.state.isVisible && (
<VariablesTableContainer>
<PropertiesTable propData={this.props.vars} propKey={'custom'} />
</VariablesTableContainer>
)}
</VariablesContainer>
);
}
}

View file

@ -0,0 +1,62 @@
/*
* 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 { mount, ReactWrapper, shallow } from 'enzyme';
import 'jest-styled-components';
import React from 'react';
import { IStackframe } from 'x-pack/plugins/apm/typings/es_schemas/Stackframe';
import { Stackframe } from '../Stackframe';
import stacktracesMock from './stacktraces.json';
describe('Stackframe', () => {
describe('when stackframe has source lines', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
const stackframe = stacktracesMock[0];
wrapper = mount(<Stackframe stackframe={stackframe} />);
});
it('should render correctly', () => {
expect(wrapper).toMatchSnapshot();
});
it('should render FrameHeading, Context and Variables', () => {
expect(wrapper.find('FrameHeading').length).toBe(1);
expect(wrapper.find('Context').length).toBe(1);
expect(wrapper.find('Variables').length).toBe(1);
});
it('should have isLibraryFrame=false as default', () => {
expect(wrapper.find('Context').prop('isLibraryFrame')).toBe(false);
});
});
describe('when stackframe does not have source lines', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
const stackframe = { line: {} } as IStackframe;
wrapper = mount(<Stackframe stackframe={stackframe} />);
});
it('should render only FrameHeading', () => {
expect(wrapper.find('FrameHeading').length).toBe(1);
expect(wrapper.find('Context').length).toBe(0);
expect(wrapper.find('Variables').length).toBe(0);
});
it('should have isLibraryFrame=false as default', () => {
expect(wrapper.find('FrameHeading').prop('isLibraryFrame')).toBe(false);
});
});
it('should respect isLibraryFrame', () => {
const stackframe = { line: {} } as IStackframe;
const wrapper = shallow(
<Stackframe stackframe={stackframe} isLibraryFrame />
);
expect(wrapper.find('FrameHeading').prop('isLibraryFrame')).toBe(true);
});
});

View file

@ -1,8 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`stactraceUtils getGroupedStackframes should collapse the library frames into a set of grouped stackframes 1`] = `
exports[`Stacktrace/index getGroupedStackframes should collapse the library frames into a set of grouped stackframes 1`] = `
Array [
Object {
"excludeFromGrouping": false,
"isLibraryFrame": false,
"stackframes": Array [
Object {
@ -29,6 +30,7 @@ Array [
],
},
Object {
"excludeFromGrouping": false,
"isLibraryFrame": true,
"stackframes": Array [
Object {
@ -94,6 +96,7 @@ Array [
],
},
Object {
"excludeFromGrouping": false,
"isLibraryFrame": false,
"stackframes": Array [
Object {
@ -141,6 +144,7 @@ Array [
],
},
Object {
"excludeFromGrouping": false,
"isLibraryFrame": true,
"stackframes": Array [
Object {
@ -286,6 +290,7 @@ Array [
],
},
Object {
"excludeFromGrouping": false,
"isLibraryFrame": false,
"stackframes": Array [
Object {
@ -312,6 +317,7 @@ Array [
],
},
Object {
"excludeFromGrouping": false,
"isLibraryFrame": true,
"stackframes": Array [
Object {
@ -357,6 +363,7 @@ Array [
],
},
Object {
"excludeFromGrouping": true,
"isLibraryFrame": true,
"stackframes": Array [
Object {
@ -372,6 +379,7 @@ Array [
],
},
Object {
"excludeFromGrouping": false,
"isLibraryFrame": true,
"stackframes": Array [
Object {

View file

@ -4,17 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Stackframe } from '../../../../../typings/es_schemas/APMDoc';
import { getGroupedStackframes, hasSourceLines } from '../stacktraceUtils';
import { IStackframe } from '../../../../../typings/es_schemas/Stackframe';
import { getGroupedStackframes } from '../index';
import stacktracesMock from './stacktraces.json';
const stackframeMockWithSource = stacktracesMock[0];
const stackframeMockWithoutSource = stacktracesMock[1];
describe('stactraceUtils', () => {
describe('Stacktrace/index', () => {
describe('getGroupedStackframes', () => {
it('should collapse the library frames into a set of grouped stackframes', () => {
const result = getGroupedStackframes(stacktracesMock as Stackframe[]);
const result = getGroupedStackframes(stacktracesMock as IStackframe[]);
expect(result).toMatchSnapshot();
});
@ -40,12 +37,13 @@ describe('stactraceUtils', () => {
exclude_from_grouping: false,
filename: 'file-d.txt'
}
] as Stackframe[];
] as IStackframe[];
const result = getGroupedStackframes(stackframes);
expect(result).toEqual([
{
excludeFromGrouping: false,
isLibraryFrame: false,
stackframes: [
{
@ -61,6 +59,7 @@ describe('stactraceUtils', () => {
]
},
{
excludeFromGrouping: false,
isLibraryFrame: true,
stackframes: [
{
@ -90,10 +89,11 @@ describe('stactraceUtils', () => {
exclude_from_grouping: false,
filename: 'file-b.txt'
}
] as Stackframe[];
] as IStackframe[];
const result = getGroupedStackframes(stackframes);
expect(result).toEqual([
{
excludeFromGrouping: false,
isLibraryFrame: false,
stackframes: [
{
@ -104,6 +104,7 @@ describe('stactraceUtils', () => {
]
},
{
excludeFromGrouping: false,
isLibraryFrame: true,
stackframes: [
{
@ -128,10 +129,11 @@ describe('stactraceUtils', () => {
exclude_from_grouping: true,
filename: 'file-b.txt'
}
] as Stackframe[];
] as IStackframe[];
const result = getGroupedStackframes(stackframes);
expect(result).toEqual([
{
excludeFromGrouping: false,
isLibraryFrame: false,
stackframes: [
{
@ -142,6 +144,7 @@ describe('stactraceUtils', () => {
]
},
{
excludeFromGrouping: true,
isLibraryFrame: false,
stackframes: [
{
@ -155,27 +158,16 @@ describe('stactraceUtils', () => {
});
it('should handle empty stackframes', () => {
const result = getGroupedStackframes([] as Stackframe[]);
const result = getGroupedStackframes([] as IStackframe[]);
expect(result).toHaveLength(0);
});
it('should handle one stackframe', () => {
const result = getGroupedStackframes([
stacktracesMock[0]
] as Stackframe[]);
] as IStackframe[]);
expect(result).toHaveLength(1);
expect(result[0].stackframes).toHaveLength(1);
});
});
describe('hasSourceLines', () => {
it('should return true given a stackframe with a source context', () => {
const result = hasSourceLines(stackframeMockWithSource as Stackframe);
expect(result).toBe(true);
});
it('should return false given a stackframe with no source context', () => {
const result = hasSourceLines(stackframeMockWithoutSource as Stackframe);
expect(result).toBe(false);
});
});
});

View file

@ -4,95 +4,85 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiTitle } from '@elastic/eui';
import { isEmpty } from 'lodash';
import React, { PureComponent } from 'react';
import { Stackframe } from '../../../../typings/es_schemas/APMDoc';
import { CodePreview } from '../../shared/CodePreview';
import { isEmpty, last } from 'lodash';
import React, { Fragment } from 'react';
import { IStackframe } from '../../../../typings/es_schemas/Stackframe';
import { EmptyMessage } from '../../shared/EmptyMessage';
// @ts-ignore
import { Ellipsis } from '../../shared/Icons';
import { FrameHeading } from './FrameHeading';
import { LibraryStackFrames } from './LibraryStackFrames';
import { getGroupedStackframes, hasSourceLines } from './stacktraceUtils';
import { Stackframe } from './Stackframe';
interface Props {
stackframes?: Stackframe[];
stackframes?: IStackframe[];
codeLanguage?: string;
}
interface State {
visibilityMap: {
[i: number]: boolean;
};
}
export class Stacktrace extends PureComponent<Props, State> {
public state = {
visibilityMap: {}
};
public componentDidMount() {
if (!this.props.stackframes) {
// Don't do anything, if there are no stackframes
return false;
}
const hasAnyAppFrames = this.props.stackframes.some(
frame => !frame.library_frame
);
if (!hasAnyAppFrames) {
// If there are no app frames available, always show the only existing group
this.setState({ visibilityMap: { 0: true } });
}
export function Stacktrace({ stackframes = [], codeLanguage }: Props) {
if (isEmpty(stackframes)) {
return <EmptyMessage heading="No stacktrace available." hideSubheading />;
}
public toggle = (i: number) =>
this.setState(({ visibilityMap }) => {
return { visibilityMap: { ...visibilityMap, [i]: !visibilityMap[i] } };
});
const groups = getGroupedStackframes(stackframes);
return (
<Fragment>
{groups.map((group, i) => {
// library frame
if (group.isLibraryFrame) {
const initialVisiblity = groups.length === 1; // if there is only a single group it should be visible initially
return (
<LibraryStackFrames
key={i}
initialVisiblity={initialVisiblity}
stackframes={group.stackframes}
codeLanguage={codeLanguage}
/>
);
}
public render() {
const { stackframes = [], codeLanguage } = this.props;
const { visibilityMap } = this.state as State;
if (isEmpty(stackframes)) {
return <EmptyMessage heading="No stacktrace available." hideSubheading />;
}
return (
<div>
<EuiTitle size="xs">
<h3>Stack traces</h3>
</EuiTitle>
{getGroupedStackframes(stackframes).map(
({ isLibraryFrame, stackframes: groupedStackframes }, i) => {
if (isLibraryFrame) {
return (
<LibraryStackFrames
key={i}
visible={visibilityMap[i]}
stackframes={groupedStackframes}
codeLanguage={codeLanguage}
onClick={() => this.toggle(i)}
/>
);
}
return groupedStackframes.map((stackframe, idx) =>
hasSourceLines(stackframe) ? (
<CodePreview
key={idx}
stackframe={stackframe}
codeLanguage={codeLanguage}
/>
) : (
<FrameHeading key={idx} stackframe={stackframe} />
)
);
}
)}
</div>
);
}
// non-library frame
return group.stackframes.map((stackframe, idx) => (
<Stackframe
key={`${i}-${idx}`}
codeLanguage={codeLanguage}
stackframe={stackframe}
/>
));
})}
</Fragment>
);
}
interface StackframesGroup {
isLibraryFrame: boolean;
excludeFromGrouping: boolean;
stackframes: IStackframe[];
}
export function getGroupedStackframes(stackframes: IStackframe[]) {
return stackframes.reduce(
(acc, stackframe) => {
const prevGroup = last(acc);
const shouldAppend =
prevGroup &&
prevGroup.isLibraryFrame === stackframe.library_frame &&
!prevGroup.excludeFromGrouping &&
!stackframe.exclude_from_grouping;
// append to group
if (shouldAppend) {
prevGroup.stackframes.push(stackframe);
return acc;
}
// create new group
acc.push({
isLibraryFrame: Boolean(stackframe.library_frame),
excludeFromGrouping: Boolean(stackframe.exclude_from_grouping),
stackframes: [stackframe]
});
return acc;
},
[] as StackframesGroup[]
);
}

View file

@ -1,48 +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 { get, isEmpty } from 'lodash';
import { Stackframe } from 'x-pack/plugins/apm/typings/es_schemas/APMDoc';
interface StackframesGroup {
isLibraryFrame: boolean;
stackframes: Stackframe[];
}
function getNextGroupIndex(stackframes: Stackframe[]): number {
const isLibraryFrame = Boolean(stackframes[0].library_frame);
const groupEndIndex =
stackframes.findIndex(
stackframe =>
isLibraryFrame !== Boolean(stackframe.library_frame) ||
Boolean(stackframe.exclude_from_grouping)
) || 1;
return groupEndIndex === -1 ? stackframes.length : groupEndIndex;
}
export function getGroupedStackframes(
stackframes: Stackframe[]
): StackframesGroup[] {
if (stackframes.length === 0) {
return [];
}
const nextGroupIndex = getNextGroupIndex(stackframes);
return [
{
isLibraryFrame: Boolean(stackframes[0].library_frame),
stackframes: stackframes.slice(0, nextGroupIndex)
},
...getGroupedStackframes(stackframes.slice(nextGroupIndex))
];
}
export function hasSourceLines(stackframe: Stackframe) {
return (
!isEmpty(stackframe.context) || !isEmpty(get(stackframe, 'line.context'))
);
}

View file

@ -28,56 +28,3 @@ export interface APMDocV2 extends APMDocV1 {
id: string;
};
}
export interface ContextService {
name: string;
agent: {
name: string;
version: string;
};
framework?: {
name: string;
version: string;
};
runtime?: {
name: string;
version: string;
};
language?: {
name: string;
version?: string;
};
[key: string]: unknown;
}
export interface Stackframe {
filename: string;
line: {
number: number;
column?: number;
context?: string;
};
abs_path?: string;
colno?: number;
context_line?: string;
function?: string;
library_frame?: boolean;
exclude_from_grouping?: boolean;
module?: string;
context?: {
post?: string[];
pre?: string[];
};
sourcemap?: {
updated?: boolean;
error?: string;
};
vars?: unknown;
orig?: {
filename?: string;
abs_path?: string;
function?: string;
lineno?: number;
colno?: number;
};
}

View file

@ -0,0 +1,52 @@
/*
* 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 interface ContextService {
name: string;
agent: {
name: string;
version: string;
};
framework?: {
name: string;
version: string;
};
runtime?: {
name: string;
version: string;
};
language?: {
name: string;
version?: string;
};
[key: string]: unknown;
}
export interface ContextSystem {
architecture?: string;
hostname?: string;
ip?: string;
platform?: string;
}
export interface ContextRequest {
url: {
full: string;
[key: string]: string;
};
method: string;
headers?: {
[key: string]: unknown;
};
[key: string]: unknown;
}
export interface ContextProcess {
pid: number;
title: string;
argv: string[];
[key: string]: unknown;
}

View file

@ -4,7 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { APMDocV1, APMDocV2, ContextService, Stackframe } from './APMDoc';
import { APMDocV1, APMDocV2 } from './APMDoc';
import {
ContextProcess,
ContextRequest,
ContextService,
ContextSystem
} from './Context';
import { IStackframe } from './Stackframe';
interface Agent {
hostname: string;
@ -18,10 +25,11 @@ interface Processor {
}
interface Context {
process?: {
pid: number;
};
process?: ContextProcess;
service: ContextService;
system?: ContextSystem;
request?: ContextRequest;
[key: string]: unknown;
}
interface Exception {
@ -31,7 +39,7 @@ interface Exception {
module?: string;
attributes?: unknown;
handled?: boolean;
stacktrace?: Stackframe[];
stacktrace?: IStackframe[];
}
interface Log {
@ -39,7 +47,7 @@ interface Log {
param_message?: string;
logger_name?: string;
level?: string;
stacktrace?: Stackframe[];
stacktrace?: IStackframe[];
}
interface ErrorV1 extends APMDocV1 {
@ -71,7 +79,6 @@ interface ErrorV2 extends APMDocV2 {
};
error: {
id: string; // ID is required in v2
timestamp: string;
culprit: string;
grouping_key: string;
// either exception or log are given

View file

@ -4,7 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { APMDocV1, APMDocV2, ContextService, Stackframe } from './APMDoc';
import { APMDocV1, APMDocV2 } from './APMDoc';
import { ContextService } from './Context';
import { IStackframe } from './Stackframe';
export interface DbContext {
instance?: string;
@ -18,7 +20,7 @@ interface Processor {
event: 'span';
}
export interface HttpContext {
interface HttpContext {
url?: string;
}
@ -49,7 +51,7 @@ export interface SpanV1 extends APMDocV1 {
type: string;
id: number; // we are manually adding span.id
parent?: string; // only v1
stacktrace?: Stackframe[];
stacktrace?: IStackframe[];
};
transaction: {
id: string;
@ -68,7 +70,7 @@ export interface SpanV2 extends APMDocV2 {
type: string;
id: number; // id will be derived from hex encoded 64 bit hex_id string in v2
hex_id: string; // only v2
stacktrace?: Stackframe[];
stacktrace?: IStackframe[];
};
transaction: {
id: string;

View file

@ -0,0 +1,59 @@
/*
* 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.
*/
interface IStackframeBase {
filename: string;
line: {
number: number;
column?: number;
context?: string;
};
abs_path?: string;
colno?: number;
context_line?: string;
function?: string;
library_frame?: boolean;
exclude_from_grouping?: boolean;
module?: string;
context?: {
post?: string[];
pre?: string[];
};
sourcemap?: {
updated?: boolean;
error?: string;
};
vars?: {
[key: string]: unknown;
};
orig?: {
filename?: string;
abs_path?: string;
function?: string;
lineno?: number;
colno?: number;
};
}
interface IStackframeWithoutLineContext extends IStackframeBase {
line: {
number: number;
column?: number;
context: undefined;
};
}
export interface IStackframeWithLineContext extends IStackframeBase {
line: {
number: number;
column?: number;
context: string;
};
}
export type IStackframe =
| IStackframeWithoutLineContext
| IStackframeWithLineContext;

View file

@ -4,35 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { APMDocV1, APMDocV2, ContextService } from './APMDoc';
import { APMDocV1, APMDocV2 } from './APMDoc';
import {
ContextProcess,
ContextRequest,
ContextService,
ContextSystem
} from './Context';
interface Processor {
name: 'transaction';
event: 'transaction';
}
interface ContextSystem {
architecture?: string;
hostname?: string;
ip?: string;
platform?: string;
}
interface Context {
process?: {
pid: number;
[key: string]: unknown;
};
process?: ContextProcess;
service: ContextService;
system?: ContextSystem;
request: {
url: {
full: string;
[key: string]: string;
};
method: string;
[key: string]: unknown;
};
request: ContextRequest;
user?: {
id: string;
username?: string;