mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[APM] Create Stackframe component, tests for Error interface and more TS (#27139)
This commit is contained in:
parent
e43030885f
commit
5a763ec71b
34 changed files with 2098 additions and 1186 deletions
|
@ -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`;
|
||||
|
|
|
@ -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, () => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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"
|
||||
>
|
||||
<anonymous>
|
||||
</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>
|
||||
`;
|
|
@ -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"
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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', () => {
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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 />
|
|
@ -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>
|
||||
`;
|
|
@ -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}
|
||||
|
|
|
@ -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": []
|
||||
}
|
||||
]
|
|
@ -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: []
|
||||
}
|
||||
];
|
|
@ -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', []));
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load diff
|
@ -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 {
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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[]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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'))
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
52
x-pack/plugins/apm/typings/es_schemas/Context.ts
Normal file
52
x-pack/plugins/apm/typings/es_schemas/Context.ts
Normal 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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
59
x-pack/plugins/apm/typings/es_schemas/Stackframe.ts
Normal file
59
x-pack/plugins/apm/typings/es_schemas/Stackframe.ts
Normal 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;
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue