mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[APM] Link from error to transaction (#27227)
This commit is contained in:
parent
7dc673721c
commit
998afde6e4
17 changed files with 650 additions and 4248 deletions
|
@ -1,42 +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 DetailView from '../index';
|
||||
import props from './props.json';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { mockMoment } from '../../../../../utils/testHelpers';
|
||||
|
||||
describe('DetailView', () => {
|
||||
beforeEach(() => {
|
||||
// Avoid timezone issues
|
||||
mockMoment();
|
||||
});
|
||||
|
||||
it('should render empty state', () => {
|
||||
const wrapper = shallow(
|
||||
<DetailView
|
||||
errorGroup={[]}
|
||||
urlParams={props.urlParams}
|
||||
location={{ state: '' }}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render with data', () => {
|
||||
const wrapper = shallow(
|
||||
<DetailView
|
||||
errorGroup={props.errorGroup}
|
||||
urlParams={props.urlParams}
|
||||
location={{ state: '' }}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* 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 { RRRRenderResponse } from 'react-redux-request';
|
||||
import { ErrorGroupAPIResponse } from 'x-pack/plugins/apm/server/lib/errors/get_error_group';
|
||||
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
|
||||
// @ts-ignore
|
||||
import { mockMoment } from '../../../../../utils/testHelpers';
|
||||
import { DetailView } from '../index';
|
||||
|
||||
describe('DetailView', () => {
|
||||
beforeEach(() => {
|
||||
// Avoid timezone issues
|
||||
mockMoment();
|
||||
});
|
||||
|
||||
it('should render empty state', () => {
|
||||
const wrapper = shallow(
|
||||
<DetailView
|
||||
errorGroup={{} as any}
|
||||
urlParams={{}}
|
||||
location={{ state: '' }}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.isEmptyRender()).toBe(true);
|
||||
});
|
||||
|
||||
it('should render Discover button', () => {
|
||||
const errorGroup: RRRRenderResponse<ErrorGroupAPIResponse> = {
|
||||
args: [],
|
||||
status: 'SUCCESS',
|
||||
data: {
|
||||
occurrencesCount: 10,
|
||||
error: ({
|
||||
'@timestamp': 'myTimestamp',
|
||||
context: {
|
||||
service: { name: 'myService' },
|
||||
user: { id: 'myUserId' },
|
||||
request: { method: 'GET', url: { full: 'myUrl' } }
|
||||
},
|
||||
error: { exception: { handled: true } },
|
||||
transaction: { id: 'myTransactionId', sampled: true }
|
||||
} as unknown) as APMError
|
||||
}
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<DetailView
|
||||
errorGroup={errorGroup}
|
||||
urlParams={{}}
|
||||
location={{ state: '' }}
|
||||
/>
|
||||
).find('DiscoverErrorButton');
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render StickyProperties', () => {
|
||||
const errorGroup: RRRRenderResponse<ErrorGroupAPIResponse> = {
|
||||
args: [],
|
||||
status: 'SUCCESS',
|
||||
data: {
|
||||
occurrencesCount: 10,
|
||||
transaction: ({
|
||||
trace: { id: 'traceId' },
|
||||
transaction: {
|
||||
type: 'myTransactionType',
|
||||
name: 'myTransactionName',
|
||||
id: 'myTransactionName'
|
||||
},
|
||||
context: {
|
||||
service: { name: 'myService' },
|
||||
user: { id: 'myUserId' },
|
||||
request: { method: 'GET', url: { full: 'myUrl' } }
|
||||
}
|
||||
} as unknown) as Transaction,
|
||||
error: ({
|
||||
'@timestamp': 'myTimestamp',
|
||||
context: {
|
||||
service: { name: 'myService' },
|
||||
user: { id: 'myUserId' },
|
||||
request: { method: 'GET', url: { full: 'myUrl' } }
|
||||
},
|
||||
error: { exception: { handled: true } },
|
||||
transaction: { id: 'myTransactionId', sampled: true }
|
||||
} as unknown) as APMError
|
||||
}
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<DetailView
|
||||
errorGroup={errorGroup}
|
||||
urlParams={{}}
|
||||
location={{ state: '' }}
|
||||
/>
|
||||
).find('StickyProperties');
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render tabs', () => {
|
||||
const errorGroup: RRRRenderResponse<ErrorGroupAPIResponse> = {
|
||||
args: [],
|
||||
status: 'SUCCESS',
|
||||
data: {
|
||||
occurrencesCount: 10,
|
||||
error: ({
|
||||
'@timestamp': 'myTimestamp',
|
||||
context: { service: {}, user: {}, request: {} }
|
||||
} as unknown) as APMError
|
||||
}
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<DetailView
|
||||
errorGroup={errorGroup}
|
||||
urlParams={{}}
|
||||
location={{ state: '' }}
|
||||
/>
|
||||
).find('EuiTabs');
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render TabContent', () => {
|
||||
const errorGroup: RRRRenderResponse<ErrorGroupAPIResponse> = {
|
||||
args: [],
|
||||
status: 'SUCCESS',
|
||||
data: {
|
||||
occurrencesCount: 10,
|
||||
error: ({
|
||||
'@timestamp': 'myTimestamp',
|
||||
context: {}
|
||||
} as unknown) as APMError
|
||||
}
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<DetailView
|
||||
errorGroup={errorGroup}
|
||||
urlParams={{}}
|
||||
location={{ state: '' }}
|
||||
/>
|
||||
).find('TabContent');
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,168 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DetailView should render Discover button 1`] = `
|
||||
<DiscoverErrorButton
|
||||
error={
|
||||
Object {
|
||||
"@timestamp": "myTimestamp",
|
||||
"context": Object {
|
||||
"request": Object {
|
||||
"method": "GET",
|
||||
"url": Object {
|
||||
"full": "myUrl",
|
||||
},
|
||||
},
|
||||
"service": Object {
|
||||
"name": "myService",
|
||||
},
|
||||
"user": Object {
|
||||
"id": "myUserId",
|
||||
},
|
||||
},
|
||||
"error": Object {
|
||||
"exception": Object {
|
||||
"handled": true,
|
||||
},
|
||||
},
|
||||
"transaction": Object {
|
||||
"id": "myTransactionId",
|
||||
"sampled": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
color="primary"
|
||||
iconSide="left"
|
||||
iconType="discoverApp"
|
||||
type="button"
|
||||
>
|
||||
View 10 occurences in Discover
|
||||
</EuiButtonEmpty>
|
||||
</DiscoverErrorButton>
|
||||
`;
|
||||
|
||||
exports[`DetailView should render StickyProperties 1`] = `
|
||||
<StickyProperties
|
||||
stickyProperties={
|
||||
Array [
|
||||
Object {
|
||||
"fieldName": "@timestamp",
|
||||
"label": "Timestamp",
|
||||
"val": "myTimestamp",
|
||||
"width": "50%",
|
||||
},
|
||||
Object {
|
||||
"fieldName": "context.request.url.full",
|
||||
"label": "URL",
|
||||
"truncated": true,
|
||||
"val": "myUrl",
|
||||
"width": "50%",
|
||||
},
|
||||
Object {
|
||||
"fieldName": "context.request.method",
|
||||
"label": "Request method",
|
||||
"val": "GET",
|
||||
"width": "25%",
|
||||
},
|
||||
Object {
|
||||
"fieldName": "error.exception.handled",
|
||||
"label": "Handled",
|
||||
"val": true,
|
||||
"width": "25%",
|
||||
},
|
||||
Object {
|
||||
"fieldName": "transaction.id",
|
||||
"label": "Transaction sample ID",
|
||||
"val": <Connect(Component)
|
||||
hash="/myService/transactions/myTransactionType/myTransactionName"
|
||||
pathname="/app/apm"
|
||||
query={
|
||||
Object {
|
||||
"traceid": "traceId",
|
||||
"transactionId": "myTransactionName",
|
||||
}
|
||||
}
|
||||
>
|
||||
myTransactionName
|
||||
</Connect(Component)>,
|
||||
"width": "25%",
|
||||
},
|
||||
Object {
|
||||
"fieldName": "context.user.id",
|
||||
"label": "User ID",
|
||||
"val": "myUserId",
|
||||
"width": "25%",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`DetailView should render TabContent 1`] = `
|
||||
<TabContent
|
||||
currentTab="exception_stacktrace"
|
||||
error={
|
||||
Object {
|
||||
"@timestamp": "myTimestamp",
|
||||
"context": Object {},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`DetailView should render tabs 1`] = `
|
||||
<EuiTabs
|
||||
expand={false}
|
||||
size="m"
|
||||
>
|
||||
<EuiTab
|
||||
disabled={false}
|
||||
isSelected={true}
|
||||
key="exception_stacktrace"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Exception stacktrace
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
disabled={false}
|
||||
isSelected={false}
|
||||
key="request"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Request
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
disabled={false}
|
||||
isSelected={false}
|
||||
key="service"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Service
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
disabled={false}
|
||||
isSelected={false}
|
||||
key="user"
|
||||
onClick={[Function]}
|
||||
>
|
||||
User
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
disabled={false}
|
||||
isSelected={false}
|
||||
key="tags"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Tags
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
disabled={false}
|
||||
isSelected={false}
|
||||
key="custom"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Custom
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
`;
|
|
@ -1,934 +0,0 @@
|
|||
{
|
||||
"errorGroup": {
|
||||
"args": {
|
||||
"serviceName": "opbeans-node",
|
||||
"errorGroupId": "c00e245c2fbebaf178fc31eeb2bb0250",
|
||||
"start": "2018-01-09T14:24:42.561Z",
|
||||
"end": "2018-01-09T14:39:42.561Z"
|
||||
},
|
||||
"data": {
|
||||
"error": {
|
||||
"@timestamp": "2018-01-09T14:39:00.274Z",
|
||||
"processor": { "event": "error", "name": "error" },
|
||||
"error": {
|
||||
"id": "c5e55dfc-09cc-4e0d-ace3-1ba4233f66eb",
|
||||
"culprit": "<anonymous> (server/coffee.js)",
|
||||
"exception": {
|
||||
"type": "TypeError",
|
||||
"stacktrace": [
|
||||
{
|
||||
"line": {
|
||||
"number": 9,
|
||||
"context": " if (req.paarms.level === 11) {"
|
||||
},
|
||||
"filename": "server/coffee.js",
|
||||
"absPath": "/app/server/coffee.js",
|
||||
"function": "<anonymous>",
|
||||
"libraryFrame": false,
|
||||
"context": {
|
||||
"pre": [
|
||||
"",
|
||||
"var express = require('express')",
|
||||
"var apm = require('elastic-apm-node')",
|
||||
"",
|
||||
"var app = module.exports = new express.Router()",
|
||||
"",
|
||||
"app.get('/is-it-coffee-time', function (req, res) {"
|
||||
],
|
||||
"post": [
|
||||
" res.send('Of course!')",
|
||||
" } else {",
|
||||
" res.send('You can\\'t have any!')",
|
||||
" }",
|
||||
"})",
|
||||
"",
|
||||
"app.get('/log-error', function (req, res) {"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "node_modules/express/lib/router/layer.js",
|
||||
"absPath": "/app/node_modules/express/lib/router/layer.js",
|
||||
"function": "handle",
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
"",
|
||||
" if (fn.length > 3) {",
|
||||
" // not a standard request handler",
|
||||
" return next();",
|
||||
" }",
|
||||
"",
|
||||
" try {"
|
||||
],
|
||||
"post": [
|
||||
" } catch (err) {",
|
||||
" next(err);",
|
||||
" }",
|
||||
"};",
|
||||
"",
|
||||
"/**",
|
||||
" * Check if this route matches `path`, if so"
|
||||
]
|
||||
},
|
||||
"line": { "number": 95, "context": " fn(req, res, next);" }
|
||||
},
|
||||
{
|
||||
"function": "next",
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
" if (layer.method && layer.method !== method) {",
|
||||
" return next(err);",
|
||||
" }",
|
||||
"",
|
||||
" if (err) {",
|
||||
" layer.handle_error(err, req, res, next);",
|
||||
" } else {"
|
||||
],
|
||||
"post": [
|
||||
" }",
|
||||
" }",
|
||||
"};",
|
||||
"",
|
||||
"/**",
|
||||
" * Add a handler for all HTTP verbs to this route.",
|
||||
" *"
|
||||
]
|
||||
},
|
||||
"line": {
|
||||
"number": 137,
|
||||
"context": " layer.handle_request(req, res, next);"
|
||||
},
|
||||
"filename": "node_modules/express/lib/router/route.js",
|
||||
"absPath": "/app/node_modules/express/lib/router/route.js"
|
||||
},
|
||||
{
|
||||
"filename": "node_modules/express/lib/router/route.js",
|
||||
"absPath": "/app/node_modules/express/lib/router/route.js",
|
||||
"function": "dispatch",
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
" var method = req.method.toLowerCase();",
|
||||
" if (method === 'head' && !this.methods['head']) {",
|
||||
" method = 'get';",
|
||||
" }",
|
||||
"",
|
||||
" req.route = this;",
|
||||
""
|
||||
],
|
||||
"post": [
|
||||
"",
|
||||
" function next(err) {",
|
||||
" // signal to exit route",
|
||||
" if (err && err === 'route') {",
|
||||
" return done();",
|
||||
" }",
|
||||
""
|
||||
]
|
||||
},
|
||||
"line": { "context": " next();", "number": 112 }
|
||||
},
|
||||
{
|
||||
"filename": "node_modules/express/lib/router/layer.js",
|
||||
"absPath": "/app/node_modules/express/lib/router/layer.js",
|
||||
"function": "handle",
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
"",
|
||||
" if (fn.length > 3) {",
|
||||
" // not a standard request handler",
|
||||
" return next();",
|
||||
" }",
|
||||
"",
|
||||
" try {"
|
||||
],
|
||||
"post": [
|
||||
" } catch (err) {",
|
||||
" next(err);",
|
||||
" }",
|
||||
"};",
|
||||
"",
|
||||
"/**",
|
||||
" * Check if this route matches `path`, if so"
|
||||
]
|
||||
},
|
||||
"line": { "number": 95, "context": " fn(req, res, next);" }
|
||||
},
|
||||
{
|
||||
"context": {
|
||||
"pre": [
|
||||
" // this should be done for the layer",
|
||||
" self.process_params(layer, paramcalled, req, res, function (err) {",
|
||||
" if (err) {",
|
||||
" return next(layerError || err);",
|
||||
" }",
|
||||
"",
|
||||
" if (route) {"
|
||||
],
|
||||
"post": [
|
||||
" }",
|
||||
"",
|
||||
" trim_prefix(layer, layerError, layerPath, path);",
|
||||
" });",
|
||||
" }",
|
||||
"",
|
||||
" function trim_prefix(layer, layerError, layerPath, path) {"
|
||||
]
|
||||
},
|
||||
"line": {
|
||||
"number": 281,
|
||||
"context":
|
||||
" return layer.handle_request(req, res, next);"
|
||||
},
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"absPath": "/app/node_modules/express/lib/router/index.js",
|
||||
"function": "<anonymous>",
|
||||
"libraryFrame": true
|
||||
},
|
||||
{
|
||||
"line": { "number": 335, "context": " return done();" },
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"absPath": "/app/node_modules/express/lib/router/index.js",
|
||||
"function": "process_params",
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"post": [
|
||||
" }",
|
||||
"",
|
||||
" var i = 0;",
|
||||
" var name;",
|
||||
" var paramIndex = 0;",
|
||||
" var key;",
|
||||
" var paramVal;"
|
||||
],
|
||||
"pre": [
|
||||
" var params = this.params;",
|
||||
"",
|
||||
" // captured parameters from the layer, keys and values",
|
||||
" var keys = layer.keys;",
|
||||
"",
|
||||
" // fast track",
|
||||
" if (!keys || keys.length === 0) {"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"absPath": "/app/node_modules/express/lib/router/index.js",
|
||||
"function": "next",
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
" // Capture one-time layer values",
|
||||
" req.params = self.mergeParams",
|
||||
" ? mergeParams(layer.params, parentParams)",
|
||||
" : layer.params;",
|
||||
" var layerPath = layer.path;",
|
||||
"",
|
||||
" // this should be done for the layer"
|
||||
],
|
||||
"post": [
|
||||
" if (err) {",
|
||||
" return next(layerError || err);",
|
||||
" }",
|
||||
"",
|
||||
" if (route) {",
|
||||
" return layer.handle_request(req, res, next);",
|
||||
" }"
|
||||
]
|
||||
},
|
||||
"line": {
|
||||
"number": 275,
|
||||
"context":
|
||||
" self.process_params(layer, paramcalled, req, res, function (err) {"
|
||||
}
|
||||
},
|
||||
{
|
||||
"line": { "context": " next();", "number": 174 },
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"absPath": "/app/node_modules/express/lib/router/index.js",
|
||||
"function": "handle",
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
" });",
|
||||
" }",
|
||||
"",
|
||||
" // setup basic req values",
|
||||
" req.baseUrl = parentUrl;",
|
||||
" req.originalUrl = req.originalUrl || req.url;",
|
||||
""
|
||||
],
|
||||
"post": [
|
||||
"",
|
||||
" function next(err) {",
|
||||
" var layerError = err === 'route'",
|
||||
" ? null",
|
||||
" : err;",
|
||||
"",
|
||||
" // remove added slash"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
" * @public",
|
||||
" */",
|
||||
"",
|
||||
"var proto = module.exports = function(options) {",
|
||||
" var opts = options || {};",
|
||||
"",
|
||||
" function router(req, res, next) {"
|
||||
],
|
||||
"post": [
|
||||
" }",
|
||||
"",
|
||||
" // mixin Router class functions",
|
||||
" setPrototypeOf(router, proto)",
|
||||
"",
|
||||
" router.params = {};",
|
||||
" router._params = [];"
|
||||
]
|
||||
},
|
||||
"line": {
|
||||
"number": 47,
|
||||
"context": " router.handle(req, res, next);"
|
||||
},
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"absPath": "/app/node_modules/express/lib/router/index.js",
|
||||
"function": "router"
|
||||
},
|
||||
{
|
||||
"filename": "node_modules/express/lib/router/layer.js",
|
||||
"absPath": "/app/node_modules/express/lib/router/layer.js",
|
||||
"function": "handle",
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
"",
|
||||
" if (fn.length > 3) {",
|
||||
" // not a standard request handler",
|
||||
" return next();",
|
||||
" }",
|
||||
"",
|
||||
" try {"
|
||||
],
|
||||
"post": [
|
||||
" } catch (err) {",
|
||||
" next(err);",
|
||||
" }",
|
||||
"};",
|
||||
"",
|
||||
"/**",
|
||||
" * Check if this route matches `path`, if so"
|
||||
]
|
||||
},
|
||||
"line": { "number": 95, "context": " fn(req, res, next);" }
|
||||
},
|
||||
{
|
||||
"absPath": "/app/node_modules/express/lib/router/index.js",
|
||||
"function": "trim_prefix",
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
" }",
|
||||
"",
|
||||
" debug('%s %s : %s', layer.name, layerPath, req.originalUrl);",
|
||||
"",
|
||||
" if (layerError) {",
|
||||
" layer.handle_error(layerError, req, res, next);",
|
||||
" } else {"
|
||||
],
|
||||
"post": [
|
||||
" }",
|
||||
" }",
|
||||
"};",
|
||||
"",
|
||||
"/**",
|
||||
" * Process any parameters for the layer.",
|
||||
" * @private"
|
||||
]
|
||||
},
|
||||
"line": {
|
||||
"number": 317,
|
||||
"context": " layer.handle_request(req, res, next);"
|
||||
},
|
||||
"filename": "node_modules/express/lib/router/index.js"
|
||||
},
|
||||
{
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
" return next(layerError || err);",
|
||||
" }",
|
||||
"",
|
||||
" if (route) {",
|
||||
" return layer.handle_request(req, res, next);",
|
||||
" }",
|
||||
""
|
||||
],
|
||||
"post": [
|
||||
" });",
|
||||
" }",
|
||||
"",
|
||||
" function trim_prefix(layer, layerError, layerPath, path) {",
|
||||
" if (layerPath.length !== 0) {",
|
||||
" // Validate path breaks on a path separator",
|
||||
" var c = path[layerPath.length]"
|
||||
]
|
||||
},
|
||||
"line": {
|
||||
"number": 284,
|
||||
"context":
|
||||
" trim_prefix(layer, layerError, layerPath, path);"
|
||||
},
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"absPath": "/app/node_modules/express/lib/router/index.js",
|
||||
"function": "<anonymous>"
|
||||
},
|
||||
{
|
||||
"function": "process_params",
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
" var params = this.params;",
|
||||
"",
|
||||
" // captured parameters from the layer, keys and values",
|
||||
" var keys = layer.keys;",
|
||||
"",
|
||||
" // fast track",
|
||||
" if (!keys || keys.length === 0) {"
|
||||
],
|
||||
"post": [
|
||||
" }",
|
||||
"",
|
||||
" var i = 0;",
|
||||
" var name;",
|
||||
" var paramIndex = 0;",
|
||||
" var key;",
|
||||
" var paramVal;"
|
||||
]
|
||||
},
|
||||
"line": { "number": 335, "context": " return done();" },
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"absPath": "/app/node_modules/express/lib/router/index.js"
|
||||
},
|
||||
{
|
||||
"line": {
|
||||
"number": 275,
|
||||
"context":
|
||||
" self.process_params(layer, paramcalled, req, res, function (err) {"
|
||||
},
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"absPath": "/app/node_modules/express/lib/router/index.js",
|
||||
"function": "next",
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
" // Capture one-time layer values",
|
||||
" req.params = self.mergeParams",
|
||||
" ? mergeParams(layer.params, parentParams)",
|
||||
" : layer.params;",
|
||||
" var layerPath = layer.path;",
|
||||
"",
|
||||
" // this should be done for the layer"
|
||||
],
|
||||
"post": [
|
||||
" if (err) {",
|
||||
" return next(layerError || err);",
|
||||
" }",
|
||||
"",
|
||||
" if (route) {",
|
||||
" return layer.handle_request(req, res, next);",
|
||||
" }"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"line": { "number": 27, "context": " next()" },
|
||||
"filename": "server.js",
|
||||
"absPath": "/app/server.js",
|
||||
"function": "<anonymous>",
|
||||
"libraryFrame": false,
|
||||
"context": {
|
||||
"post": [
|
||||
"})",
|
||||
"",
|
||||
"app.use(require('./server/coffee'))",
|
||||
"app.use('/api', require('./server/routes'))",
|
||||
"app.get('*', function (req, res) {",
|
||||
" res.sendFile(path.resolve(__dirname, 'client/build', 'index.html'))",
|
||||
"})"
|
||||
],
|
||||
"pre": [
|
||||
"app.use(require('body-parser').json())",
|
||||
"app.use(express.static('client/build'))",
|
||||
"app.use(function (req, res, next) {",
|
||||
" apm.setTag('foo', 'bar')",
|
||||
" apm.setTag('lorem', 'ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.')",
|
||||
" apm.setTag('this-is-a-very-long-tag-name-without-any-spaces', 'test')",
|
||||
" apm.setTag('multi-line', 'foo\\nbar\\nbaz')"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"function": "handle",
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"post": [
|
||||
" } catch (err) {",
|
||||
" next(err);",
|
||||
" }",
|
||||
"};",
|
||||
"",
|
||||
"/**",
|
||||
" * Check if this route matches `path`, if so"
|
||||
],
|
||||
"pre": [
|
||||
"",
|
||||
" if (fn.length > 3) {",
|
||||
" // not a standard request handler",
|
||||
" return next();",
|
||||
" }",
|
||||
"",
|
||||
" try {"
|
||||
]
|
||||
},
|
||||
"line": { "context": " fn(req, res, next);", "number": 95 },
|
||||
"filename": "node_modules/express/lib/router/layer.js",
|
||||
"absPath": "/app/node_modules/express/lib/router/layer.js"
|
||||
},
|
||||
{
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
" }",
|
||||
"",
|
||||
" debug('%s %s : %s', layer.name, layerPath, req.originalUrl);",
|
||||
"",
|
||||
" if (layerError) {",
|
||||
" layer.handle_error(layerError, req, res, next);",
|
||||
" } else {"
|
||||
],
|
||||
"post": [
|
||||
" }",
|
||||
" }",
|
||||
"};",
|
||||
"",
|
||||
"/**",
|
||||
" * Process any parameters for the layer.",
|
||||
" * @private"
|
||||
]
|
||||
},
|
||||
"line": {
|
||||
"number": 317,
|
||||
"context": " layer.handle_request(req, res, next);"
|
||||
},
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"absPath": "/app/node_modules/express/lib/router/index.js",
|
||||
"function": "trim_prefix"
|
||||
},
|
||||
{
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"absPath": "/app/node_modules/express/lib/router/index.js",
|
||||
"function": "<anonymous>",
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
" return next(layerError || err);",
|
||||
" }",
|
||||
"",
|
||||
" if (route) {",
|
||||
" return layer.handle_request(req, res, next);",
|
||||
" }",
|
||||
""
|
||||
],
|
||||
"post": [
|
||||
" });",
|
||||
" }",
|
||||
"",
|
||||
" function trim_prefix(layer, layerError, layerPath, path) {",
|
||||
" if (layerPath.length !== 0) {",
|
||||
" // Validate path breaks on a path separator",
|
||||
" var c = path[layerPath.length]"
|
||||
]
|
||||
},
|
||||
"line": {
|
||||
"number": 284,
|
||||
"context":
|
||||
" trim_prefix(layer, layerError, layerPath, path);"
|
||||
}
|
||||
},
|
||||
{
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"post": [
|
||||
" }",
|
||||
"",
|
||||
" var i = 0;",
|
||||
" var name;",
|
||||
" var paramIndex = 0;",
|
||||
" var key;",
|
||||
" var paramVal;"
|
||||
],
|
||||
"pre": [
|
||||
" var params = this.params;",
|
||||
"",
|
||||
" // captured parameters from the layer, keys and values",
|
||||
" var keys = layer.keys;",
|
||||
"",
|
||||
" // fast track",
|
||||
" if (!keys || keys.length === 0) {"
|
||||
]
|
||||
},
|
||||
"line": { "number": 335, "context": " return done();" },
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"absPath": "/app/node_modules/express/lib/router/index.js",
|
||||
"function": "process_params"
|
||||
},
|
||||
{
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
" // Capture one-time layer values",
|
||||
" req.params = self.mergeParams",
|
||||
" ? mergeParams(layer.params, parentParams)",
|
||||
" : layer.params;",
|
||||
" var layerPath = layer.path;",
|
||||
"",
|
||||
" // this should be done for the layer"
|
||||
],
|
||||
"post": [
|
||||
" if (err) {",
|
||||
" return next(layerError || err);",
|
||||
" }",
|
||||
"",
|
||||
" if (route) {",
|
||||
" return layer.handle_request(req, res, next);",
|
||||
" }"
|
||||
]
|
||||
},
|
||||
"line": {
|
||||
"number": 275,
|
||||
"context":
|
||||
" self.process_params(layer, paramcalled, req, res, function (err) {"
|
||||
},
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"absPath": "/app/node_modules/express/lib/router/index.js",
|
||||
"function": "next"
|
||||
},
|
||||
{
|
||||
"absPath":
|
||||
"/app/node_modules/elastic-apm-node/lib/instrumentation/modules/express.js",
|
||||
"function": "nextHook",
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
" return function serveStatic (req, res, next) {",
|
||||
" req._elastic_apm_static = true",
|
||||
"",
|
||||
" return origServeStatic(req, res, nextHook)",
|
||||
"",
|
||||
" function nextHook (err) {",
|
||||
" if (!err) req._elastic_apm_static = false"
|
||||
],
|
||||
"post": [
|
||||
" }",
|
||||
" }",
|
||||
" }",
|
||||
" })",
|
||||
"",
|
||||
" return express",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"line": {
|
||||
"number": 90,
|
||||
"context": " return next.apply(this, arguments)"
|
||||
},
|
||||
"filename":
|
||||
"node_modules/elastic-apm-node/lib/instrumentation/modules/express.js"
|
||||
},
|
||||
{
|
||||
"filename": "node_modules/serve-static/index.js",
|
||||
"absPath": "/app/node_modules/serve-static/index.js",
|
||||
"function": "error",
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"post": [
|
||||
" })",
|
||||
"",
|
||||
" // pipe",
|
||||
" stream.pipe(res)",
|
||||
" }",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"pre": [
|
||||
" // forward errors",
|
||||
" stream.on('error', function error (err) {",
|
||||
" if (forwardError || !(err.statusCode < 500)) {",
|
||||
" next(err)",
|
||||
" return",
|
||||
" }",
|
||||
""
|
||||
]
|
||||
},
|
||||
"line": { "number": 121, "context": " next()" }
|
||||
},
|
||||
{
|
||||
"filename": "events.js",
|
||||
"absPath": "events.js",
|
||||
"function": "emitOne",
|
||||
"libraryFrame": true,
|
||||
"line": { "number": 96 }
|
||||
},
|
||||
{
|
||||
"filename": "events.js",
|
||||
"absPath": "events.js",
|
||||
"function": "emit",
|
||||
"libraryFrame": true,
|
||||
"line": { "number": 188 }
|
||||
},
|
||||
{
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
" * @param {Error} [err]",
|
||||
" * @private",
|
||||
" */",
|
||||
"",
|
||||
"SendStream.prototype.error = function error (status, err) {",
|
||||
" // emit if listeners instead of responding",
|
||||
" if (hasListeners(this, 'error')) {"
|
||||
],
|
||||
"post": [
|
||||
" expose: false",
|
||||
" }))",
|
||||
" }",
|
||||
"",
|
||||
" var res = this.res",
|
||||
" var msg = statuses[status] || String(status)",
|
||||
" var doc = createHtmlDocument('Error', escapeHtml(msg))"
|
||||
]
|
||||
},
|
||||
"line": {
|
||||
"number": 270,
|
||||
"context":
|
||||
" return this.emit('error', createError(status, err, {"
|
||||
},
|
||||
"filename": "node_modules/send/index.js",
|
||||
"absPath": "/app/node_modules/send/index.js",
|
||||
"function": "error"
|
||||
},
|
||||
{
|
||||
"function": "onStatError",
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
" */",
|
||||
"",
|
||||
"SendStream.prototype.onStatError = function onStatError (error) {",
|
||||
" switch (error.code) {",
|
||||
" case 'ENAMETOOLONG':",
|
||||
" case 'ENOENT':",
|
||||
" case 'ENOTDIR':"
|
||||
],
|
||||
"post": [
|
||||
" break",
|
||||
" default:",
|
||||
" this.error(500, error)",
|
||||
" break",
|
||||
" }",
|
||||
"}",
|
||||
""
|
||||
]
|
||||
},
|
||||
"line": {
|
||||
"number": 421,
|
||||
"context": " this.error(404, error)"
|
||||
},
|
||||
"filename": "node_modules/send/index.js",
|
||||
"absPath": "/app/node_modules/send/index.js"
|
||||
},
|
||||
{
|
||||
"line": {
|
||||
"number": 736,
|
||||
"context": " ? self.onStatError(err)"
|
||||
},
|
||||
"filename": "node_modules/send/index.js",
|
||||
"absPath": "/app/node_modules/send/index.js",
|
||||
"function": "next",
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
" self.emit('file', path, stat)",
|
||||
" self.send(path, stat)",
|
||||
" })",
|
||||
"",
|
||||
" function next (err) {",
|
||||
" if (self._extensions.length <= i) {",
|
||||
" return err"
|
||||
],
|
||||
"post": [
|
||||
" : self.error(404)",
|
||||
" }",
|
||||
"",
|
||||
" var p = path + '.' + self._extensions[i++]",
|
||||
"",
|
||||
" debug('stat \"%s\"', p)",
|
||||
" fs.stat(p, function (err, stat) {"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "node_modules/send/index.js",
|
||||
"absPath": "/app/node_modules/send/index.js",
|
||||
"function": "onstat",
|
||||
"libraryFrame": true,
|
||||
"context": {
|
||||
"pre": [
|
||||
" var i = 0",
|
||||
" var self = this",
|
||||
"",
|
||||
" debug('stat \"%s\"', path)",
|
||||
" fs.stat(path, function onstat (err, stat) {",
|
||||
" if (err && err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep) {",
|
||||
" // not found, check extensions"
|
||||
],
|
||||
"post": [
|
||||
" }",
|
||||
" if (err) return self.onStatError(err)",
|
||||
" if (stat.isDirectory()) return self.redirect(path)",
|
||||
" self.emit('file', path, stat)",
|
||||
" self.send(path, stat)",
|
||||
" })",
|
||||
""
|
||||
]
|
||||
},
|
||||
"line": { "number": 725, "context": " return next(err)" }
|
||||
},
|
||||
{
|
||||
"context": {
|
||||
"pre": [
|
||||
" var trans = this.currentTransaction",
|
||||
"",
|
||||
" return elasticAPMCallbackWrapper",
|
||||
"",
|
||||
" function elasticAPMCallbackWrapper () {",
|
||||
" var prev = ins.currentTransaction",
|
||||
" ins.currentTransaction = trans"
|
||||
],
|
||||
"post": [
|
||||
" ins.currentTransaction = prev",
|
||||
" return result",
|
||||
" }",
|
||||
"}",
|
||||
"",
|
||||
"Instrumentation.prototype._recoverTransaction = function (trans) {",
|
||||
" if (this.currentTransaction === trans) return"
|
||||
]
|
||||
},
|
||||
"line": {
|
||||
"number": 116,
|
||||
"context": " var result = original.apply(this, arguments)"
|
||||
},
|
||||
"filename":
|
||||
"node_modules/elastic-apm-node/lib/instrumentation/index.js",
|
||||
"absPath":
|
||||
"/app/node_modules/elastic-apm-node/lib/instrumentation/index.js",
|
||||
"function": "elasticAPMCallbackWrapper",
|
||||
"libraryFrame": true
|
||||
},
|
||||
{
|
||||
"filename": "fs.js",
|
||||
"absPath": "fs.js",
|
||||
"function": "FSReqWrap.oncomplete",
|
||||
"libraryFrame": true,
|
||||
"line": { "number": 123 }
|
||||
}
|
||||
],
|
||||
"message": "Cannot read property 'level' of undefined"
|
||||
},
|
||||
"groupingKey": "c00e245c2fbebaf178fc31eeb2bb0250"
|
||||
},
|
||||
"context": {
|
||||
"request": {
|
||||
"http_version": "1.1",
|
||||
"method": "GET",
|
||||
"url": {
|
||||
"raw": "/is-it-coffee-time",
|
||||
"hostname": "opbeans-node",
|
||||
"port": "3000",
|
||||
"pathname": "/is-it-coffee-time"
|
||||
},
|
||||
"socket": {
|
||||
"remote_address": "::ffff:172.19.0.7",
|
||||
"encrypted": false
|
||||
},
|
||||
"headers": {
|
||||
"user-agent": "workload/2.4.3",
|
||||
"host": "opbeans-node:3000",
|
||||
"connection": "close"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status_code": 200,
|
||||
"headers": {},
|
||||
"headers_sent": false,
|
||||
"finished": false
|
||||
},
|
||||
"system": {
|
||||
"architecture": "x64",
|
||||
"platform": "linux",
|
||||
"hostname": "b4cb1df7e088"
|
||||
},
|
||||
"tags": {
|
||||
"foo": "bar",
|
||||
"lorem":
|
||||
"ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.",
|
||||
"this-is-a-very-long-tag-name-without-any-spaces": "test",
|
||||
"multi-line": "foo\nbar\nbaz"
|
||||
},
|
||||
"custom": {},
|
||||
"service": {
|
||||
"language": { "name": "javascript" },
|
||||
"runtime": { "name": "node", "version": "v6.12.0" },
|
||||
"framework": { "name": "express", "version": "4.16.2" },
|
||||
"name": "opbeans-node",
|
||||
"agent": { "name": "nodejs", "version": "0.9.0" }
|
||||
},
|
||||
"process": {
|
||||
"pid": 36,
|
||||
"title": "node /app/server.js",
|
||||
"argv": [
|
||||
"/usr/local/bin/node",
|
||||
"/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js"
|
||||
]
|
||||
},
|
||||
"user": {}
|
||||
},
|
||||
"beat": {
|
||||
"name": "8be8857dbeda",
|
||||
"hostname": "8be8857dbeda",
|
||||
"version": "7.0.0-alpha1"
|
||||
}
|
||||
},
|
||||
"occurrencesCount": 18
|
||||
},
|
||||
"status": "SUCCESS"
|
||||
},
|
||||
"urlParams": {
|
||||
"page": 0,
|
||||
"serviceName": "opbeans-node",
|
||||
"transactionType": "request",
|
||||
"errorGroupId": "c00e245c2fbebaf178fc31eeb2bb0250",
|
||||
"start": "2018-01-09T14:24:42.561Z",
|
||||
"end": "2018-01-09T14:39:42.561Z"
|
||||
}
|
||||
}
|
|
@ -1,225 +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 { EuiButtonEmpty } from '@elastic/eui';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
unit,
|
||||
units,
|
||||
px,
|
||||
colors,
|
||||
borderRadius
|
||||
} from '../../../../style/variables';
|
||||
import { get, capitalize, isEmpty } from 'lodash';
|
||||
import { STATUS } from '../../../../constants';
|
||||
import { StickyProperties } from '../../../shared/StickyProperties';
|
||||
import { Tab, HeaderMedium } from '../../../shared/UIComponents';
|
||||
import { DiscoverErrorButton } from '../../../shared/DiscoverButtons/DiscoverErrorButton';
|
||||
import {
|
||||
PropertiesTable,
|
||||
getPropertyTabNames
|
||||
} from '../../../shared/PropertiesTable';
|
||||
import { Stacktrace } from '../../../shared/Stacktrace';
|
||||
import {
|
||||
SERVICE_AGENT_NAME,
|
||||
SERVICE_LANGUAGE_NAME,
|
||||
USER_ID,
|
||||
REQUEST_URL_FULL,
|
||||
REQUEST_METHOD,
|
||||
ERROR_EXC_HANDLED,
|
||||
ERROR_LOG_STACKTRACE,
|
||||
ERROR_EXC_STACKTRACE
|
||||
} from '../../../../../common/constants';
|
||||
import { fromQuery, toQuery, history } from '../../../../utils/url';
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
border: 1px solid ${colors.gray4};
|
||||
border-radius: ${borderRadius};
|
||||
margin-top: ${px(units.plus)};
|
||||
`;
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: ${px(units.plus)} ${px(units.plus)} 0;
|
||||
margin-bottom: ${px(unit)};
|
||||
`;
|
||||
|
||||
const TabContainer = styled.div`
|
||||
padding: 0 ${px(units.plus)};
|
||||
border-bottom: 1px solid ${colors.gray4};
|
||||
`;
|
||||
|
||||
const TabContentContainer = styled.div`
|
||||
padding: ${px(units.plus)} ${px(units.plus)} 0;
|
||||
`;
|
||||
|
||||
const EXC_STACKTRACE_TAB = 'exception_stacktrace';
|
||||
const LOG_STACKTRACE_TAB = 'log_stacktrace';
|
||||
|
||||
// Ensure the selected tab exists or use the first
|
||||
function getCurrentTab(tabs = [], selectedTab) {
|
||||
return tabs.includes(selectedTab) ? selectedTab : tabs[0];
|
||||
}
|
||||
|
||||
function getTabs(context, logStackframes) {
|
||||
const dynamicProps = Object.keys(context);
|
||||
return [
|
||||
...(logStackframes ? [LOG_STACKTRACE_TAB] : []),
|
||||
EXC_STACKTRACE_TAB,
|
||||
...getPropertyTabNames(dynamicProps)
|
||||
];
|
||||
}
|
||||
|
||||
function DetailView({ errorGroup, urlParams, location }) {
|
||||
if (errorGroup.status !== STATUS.SUCCESS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isEmpty(errorGroup.data.error)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stickyProperties = [
|
||||
{
|
||||
fieldName: '@timestamp',
|
||||
label: 'Timestamp',
|
||||
val: get(errorGroup.data.error, '@timestamp'),
|
||||
width: '50%'
|
||||
},
|
||||
{
|
||||
fieldName: REQUEST_URL_FULL,
|
||||
label: 'URL',
|
||||
val: get(errorGroup.data.error, REQUEST_URL_FULL, 'N/A'),
|
||||
truncated: true,
|
||||
width: '50%'
|
||||
},
|
||||
{
|
||||
fieldName: REQUEST_METHOD,
|
||||
label: 'Request method',
|
||||
val: get(errorGroup.data.error, REQUEST_METHOD, 'N/A'),
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
fieldName: ERROR_EXC_HANDLED,
|
||||
label: 'Handled',
|
||||
val: get(errorGroup.data.error, ERROR_EXC_HANDLED, 'N/A'),
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
fieldName: USER_ID,
|
||||
label: 'User ID',
|
||||
val: get(errorGroup.data.error, USER_ID, 'N/A'),
|
||||
width: '50%'
|
||||
}
|
||||
];
|
||||
|
||||
const excStackframes = get(errorGroup.data.error, ERROR_EXC_STACKTRACE);
|
||||
const logStackframes = get(errorGroup.data.error, ERROR_LOG_STACKTRACE);
|
||||
const codeLanguage = get(errorGroup.data.error, SERVICE_LANGUAGE_NAME);
|
||||
const context = get(errorGroup.data.error, 'context', {});
|
||||
const tabs = getTabs(context, logStackframes);
|
||||
const currentTab = getCurrentTab(tabs, urlParams.detailTab);
|
||||
const occurencesCount = errorGroup.data.occurrencesCount;
|
||||
const agentName = get(errorGroup.data.error, SERVICE_AGENT_NAME);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<HeaderContainer>
|
||||
<HeaderMedium
|
||||
css={`
|
||||
margin: 0;
|
||||
`}
|
||||
>
|
||||
Error occurrence
|
||||
</HeaderMedium>
|
||||
<DiscoverErrorButton
|
||||
error={errorGroup.data.error}
|
||||
kuery={urlParams.kuery}
|
||||
>
|
||||
<EuiButtonEmpty iconType="discoverApp">
|
||||
{`View ${occurencesCount} occurences in Discover`}
|
||||
</EuiButtonEmpty>
|
||||
</DiscoverErrorButton>
|
||||
</HeaderContainer>
|
||||
|
||||
<TabContentContainer>
|
||||
<StickyProperties stickyProperties={stickyProperties} />
|
||||
</TabContentContainer>
|
||||
|
||||
<TabContainer>
|
||||
{tabs.map(key => {
|
||||
return (
|
||||
<Tab
|
||||
onClick={() => {
|
||||
history.replace({
|
||||
...location,
|
||||
search: fromQuery({
|
||||
...toQuery(location.search),
|
||||
detailTab: key
|
||||
})
|
||||
});
|
||||
}}
|
||||
selected={currentTab === key}
|
||||
key={key}
|
||||
>
|
||||
{capitalize(key.replace('_', ' '))}
|
||||
</Tab>
|
||||
);
|
||||
})}
|
||||
</TabContainer>
|
||||
|
||||
<TabContentContainer>
|
||||
<TabContent
|
||||
currentTab={currentTab}
|
||||
logStackframes={logStackframes}
|
||||
excStackframes={excStackframes}
|
||||
codeLanguage={codeLanguage}
|
||||
errorGroup={errorGroup}
|
||||
agentName={agentName}
|
||||
/>
|
||||
</TabContentContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function TabContent({
|
||||
currentTab,
|
||||
logStackframes,
|
||||
excStackframes,
|
||||
codeLanguage,
|
||||
errorGroup,
|
||||
agentName
|
||||
}) {
|
||||
switch (currentTab) {
|
||||
case LOG_STACKTRACE_TAB:
|
||||
return (
|
||||
<Stacktrace stackframes={logStackframes} codeLanguage={codeLanguage} />
|
||||
);
|
||||
case EXC_STACKTRACE_TAB:
|
||||
return (
|
||||
<Stacktrace stackframes={excStackframes} codeLanguage={codeLanguage} />
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<PropertiesTable
|
||||
propData={errorGroup.data.error.context[currentTab]}
|
||||
propKey={currentTab}
|
||||
agentName={agentName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DetailView.propTypes = {
|
||||
location: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default DetailView;
|
|
@ -0,0 +1,257 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButtonEmpty,
|
||||
EuiSpacer,
|
||||
EuiTab,
|
||||
EuiTabs,
|
||||
EuiTitle
|
||||
} from '@elastic/eui';
|
||||
import { capitalize, get } from 'lodash';
|
||||
import React from 'react';
|
||||
import { RRRRenderResponse } from 'react-redux-request';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
ERROR_EXC_STACKTRACE,
|
||||
ERROR_LOG_STACKTRACE
|
||||
} from 'x-pack/plugins/apm/common/constants';
|
||||
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||
import { ErrorGroupAPIResponse } from 'x-pack/plugins/apm/server/lib/errors/get_error_group';
|
||||
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
|
||||
import { IStackframe } from 'x-pack/plugins/apm/typings/es_schemas/Stackframe';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
|
||||
import {
|
||||
ERROR_EXC_HANDLED,
|
||||
REQUEST_METHOD,
|
||||
REQUEST_URL_FULL,
|
||||
TRACE_ID,
|
||||
TRANSACTION_ID,
|
||||
USER_ID
|
||||
} from '../../../../../common/constants';
|
||||
import { STATUS } from '../../../../constants';
|
||||
import {
|
||||
borderRadius,
|
||||
colors,
|
||||
px,
|
||||
unit,
|
||||
units
|
||||
} from '../../../../style/variables';
|
||||
import { fromQuery, history, toQuery } from '../../../../utils/url';
|
||||
import { KibanaLink, legacyEncodeURIComponent } from '../../../../utils/url';
|
||||
import { DiscoverErrorButton } from '../../../shared/DiscoverButtons/DiscoverErrorButton';
|
||||
import {
|
||||
getPropertyTabNames,
|
||||
PropertiesTable
|
||||
} from '../../../shared/PropertiesTable';
|
||||
import { Stacktrace } from '../../../shared/Stacktrace';
|
||||
import { StickyProperties } from '../../../shared/StickyProperties';
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
border: 1px solid ${colors.gray4};
|
||||
border-radius: ${borderRadius};
|
||||
margin-top: ${px(units.plus)};
|
||||
`;
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: ${px(units.plus)} ${px(units.plus)} 0;
|
||||
margin-bottom: ${px(unit)};
|
||||
`;
|
||||
|
||||
const PaddedContainer = styled.div`
|
||||
padding: ${px(units.plus)} ${px(units.plus)} 0;
|
||||
`;
|
||||
|
||||
const EXC_STACKTRACE_TAB = 'exception_stacktrace';
|
||||
const LOG_STACKTRACE_TAB = 'log_stacktrace';
|
||||
|
||||
interface Props {
|
||||
errorGroup: RRRRenderResponse<ErrorGroupAPIResponse>;
|
||||
urlParams: IUrlParams;
|
||||
location: any;
|
||||
}
|
||||
|
||||
export function DetailView({ errorGroup, urlParams, location }: Props) {
|
||||
if (errorGroup.status !== STATUS.SUCCESS) {
|
||||
return null;
|
||||
}
|
||||
const { transaction, error, occurrencesCount } = errorGroup.data;
|
||||
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const transactionLink = getTransactionLink(error, transaction);
|
||||
const stickyProperties = [
|
||||
{
|
||||
fieldName: '@timestamp',
|
||||
label: 'Timestamp',
|
||||
val: error['@timestamp'],
|
||||
width: '50%'
|
||||
},
|
||||
{
|
||||
fieldName: REQUEST_URL_FULL,
|
||||
label: 'URL',
|
||||
val: get(error, REQUEST_URL_FULL, 'N/A'),
|
||||
truncated: true,
|
||||
width: '50%'
|
||||
},
|
||||
{
|
||||
fieldName: REQUEST_METHOD,
|
||||
label: 'Request method',
|
||||
val: get(error, REQUEST_METHOD, 'N/A'),
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
fieldName: ERROR_EXC_HANDLED,
|
||||
label: 'Handled',
|
||||
val: get(error, ERROR_EXC_HANDLED, 'N/A'),
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
fieldName: TRANSACTION_ID,
|
||||
label: 'Transaction sample ID',
|
||||
val: transactionLink || 'N/A',
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
fieldName: USER_ID,
|
||||
label: 'User ID',
|
||||
val: get(error, USER_ID, 'N/A'),
|
||||
width: '25%'
|
||||
}
|
||||
];
|
||||
|
||||
const tabs = getTabs(error);
|
||||
const currentTab = getCurrentTab(tabs, urlParams.detailTab);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<HeaderContainer>
|
||||
<EuiTitle size="s">
|
||||
<h3>Error occurence</h3>
|
||||
</EuiTitle>
|
||||
<DiscoverErrorButton error={error} kuery={urlParams.kuery}>
|
||||
<EuiButtonEmpty iconType="discoverApp">
|
||||
{`View ${occurrencesCount} occurences in Discover`}
|
||||
</EuiButtonEmpty>
|
||||
</DiscoverErrorButton>
|
||||
</HeaderContainer>
|
||||
|
||||
<PaddedContainer>
|
||||
<StickyProperties stickyProperties={stickyProperties} />
|
||||
</PaddedContainer>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<EuiTabs>
|
||||
{tabs.map(key => {
|
||||
return (
|
||||
<EuiTab
|
||||
onClick={() => {
|
||||
history.replace({
|
||||
...location,
|
||||
search: fromQuery({
|
||||
...toQuery(location.search),
|
||||
detailTab: key
|
||||
})
|
||||
});
|
||||
}}
|
||||
isSelected={currentTab === key}
|
||||
key={key}
|
||||
>
|
||||
{capitalize(key.replace('_', ' '))}
|
||||
</EuiTab>
|
||||
);
|
||||
})}
|
||||
</EuiTabs>
|
||||
|
||||
<PaddedContainer>
|
||||
<TabContent error={error} currentTab={currentTab} />
|
||||
</PaddedContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function getTransactionLink(error: APMError, transaction?: Transaction) {
|
||||
if (!transaction || !get(error, 'transaction.sampled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path = `/${
|
||||
transaction.context.service.name
|
||||
}/transactions/${legacyEncodeURIComponent(
|
||||
transaction.transaction.type
|
||||
)}/${legacyEncodeURIComponent(transaction.transaction.name)}`;
|
||||
|
||||
return (
|
||||
<KibanaLink
|
||||
pathname={'/app/apm'}
|
||||
hash={path}
|
||||
query={{
|
||||
transactionId: transaction.transaction.id,
|
||||
traceid: get(transaction, TRACE_ID)
|
||||
}}
|
||||
>
|
||||
{transaction.transaction.id}
|
||||
</KibanaLink>
|
||||
);
|
||||
}
|
||||
|
||||
type MaybeStackframes = IStackframe[] | undefined;
|
||||
|
||||
export function TabContent({
|
||||
error,
|
||||
currentTab
|
||||
}: {
|
||||
error: APMError;
|
||||
currentTab?: string;
|
||||
}) {
|
||||
const codeLanguage = error.context.service.name;
|
||||
const agentName = error.context.service.agent.name;
|
||||
const excStackframes: MaybeStackframes = get(error, ERROR_EXC_STACKTRACE);
|
||||
const logStackframes: MaybeStackframes = get(error, ERROR_LOG_STACKTRACE);
|
||||
|
||||
switch (currentTab) {
|
||||
case LOG_STACKTRACE_TAB:
|
||||
case undefined:
|
||||
return (
|
||||
<Stacktrace stackframes={logStackframes} codeLanguage={codeLanguage} />
|
||||
);
|
||||
case EXC_STACKTRACE_TAB:
|
||||
return (
|
||||
<Stacktrace stackframes={excStackframes} codeLanguage={codeLanguage} />
|
||||
);
|
||||
default:
|
||||
const propData = error.context[currentTab] as any;
|
||||
return (
|
||||
<PropertiesTable
|
||||
propData={propData}
|
||||
propKey={currentTab}
|
||||
agentName={agentName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the selected tab exists or use the first
|
||||
export function getCurrentTab(tabs: string[] = [], selectedTab?: string) {
|
||||
return tabs.includes(selectedTab!) ? selectedTab : tabs[0];
|
||||
}
|
||||
|
||||
export function getTabs(error: APMError) {
|
||||
const hasLogStacktrace = get(error, ERROR_LOG_STACKTRACE, []).length > 0;
|
||||
const contextKeys = Object.keys(error.context);
|
||||
return [
|
||||
...(hasLogStacktrace ? [LOG_STACKTRACE_TAB] : []),
|
||||
EXC_STACKTRACE_TAB,
|
||||
...getPropertyTabNames(contextKeys)
|
||||
];
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import ErrorGroupDetails from './view';
|
||||
import { ErrorGroupDetails } from './view';
|
||||
import { getUrlParams } from '../../../store/urlParams';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
|
|
|
@ -4,32 +4,32 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiBadge, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { get } from 'lodash';
|
||||
import React, { Fragment } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
import { get } from 'lodash';
|
||||
import { HeaderLarge } from '../../shared/UIComponents';
|
||||
import DetailView from './DetailView';
|
||||
import Distribution from './Distribution';
|
||||
import { KueryBar } from '../../shared/KueryBar';
|
||||
|
||||
import { EuiText, EuiBadge, EuiSpacer } from '@elastic/eui';
|
||||
import {
|
||||
unit,
|
||||
units,
|
||||
px,
|
||||
colors,
|
||||
fontFamilyCode,
|
||||
fontSizes
|
||||
} from '../../../style/variables';
|
||||
import {
|
||||
ERROR_CULPRIT,
|
||||
ERROR_LOG_MESSAGE,
|
||||
ERROR_EXC_HANDLED,
|
||||
ERROR_EXC_MESSAGE,
|
||||
ERROR_EXC_HANDLED
|
||||
ERROR_LOG_MESSAGE
|
||||
} from '../../../../common/constants';
|
||||
import { ErrorDistributionRequest } from '../../../store/reactReduxRequest/errorDistribution';
|
||||
import { ErrorGroupDetailsRequest } from '../../../store/reactReduxRequest/errorGroup';
|
||||
import { IUrlParams } from '../../../store/urlParams';
|
||||
import {
|
||||
colors,
|
||||
fontFamilyCode,
|
||||
fontSizes,
|
||||
px,
|
||||
unit,
|
||||
units
|
||||
} from '../../../style/variables';
|
||||
// @ts-ignore
|
||||
import { KueryBar } from '../../shared/KueryBar';
|
||||
import { DetailView } from './DetailView';
|
||||
// @ts-ignore
|
||||
import Distribution from './Distribution';
|
||||
|
||||
const Titles = styled.div`
|
||||
margin-bottom: ${px(units.plus)};
|
||||
|
@ -57,7 +57,7 @@ const Culprit = styled.div`
|
|||
font-family: ${fontFamilyCode};
|
||||
`;
|
||||
|
||||
function getShortGroupId(errorGroupId) {
|
||||
function getShortGroupId(errorGroupId?: string) {
|
||||
if (!errorGroupId) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
@ -65,7 +65,12 @@ function getShortGroupId(errorGroupId) {
|
|||
return errorGroupId.slice(0, 5);
|
||||
}
|
||||
|
||||
function ErrorGroupDetails({ urlParams, location }) {
|
||||
interface Props {
|
||||
urlParams: IUrlParams;
|
||||
location: any;
|
||||
}
|
||||
|
||||
export function ErrorGroupDetails({ urlParams, location }: Props) {
|
||||
return (
|
||||
<ErrorGroupDetailsRequest
|
||||
urlParams={urlParams}
|
||||
|
@ -80,12 +85,16 @@ function ErrorGroupDetails({ urlParams, location }) {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<HeaderLarge>
|
||||
Error group {getShortGroupId(urlParams.errorGroupId)}
|
||||
{isUnhandled && (
|
||||
<UnhandledBadge color="warning">Unhandled</UnhandledBadge>
|
||||
)}
|
||||
</HeaderLarge>
|
||||
<EuiTitle>
|
||||
<span>
|
||||
Error group {getShortGroupId(urlParams.errorGroupId)}
|
||||
{isUnhandled && (
|
||||
<UnhandledBadge color="warning">Unhandled</UnhandledBadge>
|
||||
)}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<KueryBar />
|
||||
|
||||
|
@ -124,9 +133,3 @@ function ErrorGroupDetails({ urlParams, location }) {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ErrorGroupDetails.propTypes = {
|
||||
location: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default ErrorGroupDetails;
|
|
@ -4,13 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiSpacer,
|
||||
// @ts-ignore
|
||||
EuiTab,
|
||||
// @ts-ignore
|
||||
EuiTabs
|
||||
} from '@elastic/eui';
|
||||
import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
|
||||
import { capitalize, first, get } from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
|
||||
import { DiscoverButton } from './DiscoverButton';
|
||||
|
||||
function getDiscoverQuery(error: APMError, kuery: string) {
|
||||
function getDiscoverQuery(error: APMError, kuery?: string) {
|
||||
const serviceName = error.context.service.name;
|
||||
const groupId = error.error.grouping_key;
|
||||
let query = `${SERVICE_NAME}:"${serviceName}" AND ${ERROR_GROUP_ID}:"${groupId}"`;
|
||||
|
@ -32,9 +32,9 @@ function getDiscoverQuery(error: APMError, kuery: string) {
|
|||
};
|
||||
}
|
||||
|
||||
export const DiscoverErrorButton: React.SFC<{
|
||||
const DiscoverErrorButton: React.SFC<{
|
||||
readonly error: APMError;
|
||||
readonly kuery: string;
|
||||
readonly kuery?: string;
|
||||
}> = ({ error, kuery, children }) => {
|
||||
return (
|
||||
<DiscoverButton
|
||||
|
@ -43,3 +43,5 @@ export const DiscoverErrorButton: React.SFC<{
|
|||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { DiscoverErrorButton };
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
} from '../../../style/variables';
|
||||
|
||||
export interface IStickyProperty {
|
||||
val: any;
|
||||
val: React.ReactNode | Date;
|
||||
label: string;
|
||||
fieldName?: string;
|
||||
width?: 0 | string;
|
||||
|
@ -90,7 +90,7 @@ function getPropertyValue({
|
|||
truncated = false
|
||||
}: Partial<IStickyProperty>) {
|
||||
if (fieldName === '@timestamp') {
|
||||
return <TimestampValue timestamp={val} />;
|
||||
return <TimestampValue timestamp={val as Date} />;
|
||||
}
|
||||
|
||||
if (truncated) {
|
||||
|
|
|
@ -1,52 +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 styled from 'styled-components';
|
||||
import { colors, fontSizes, px, unit, units } from '../../style/variables';
|
||||
|
||||
export const HeaderContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: ${px(units.plus)};
|
||||
|
||||
h1 {
|
||||
font-size: ${fontSizes.xxlarge};
|
||||
}
|
||||
`;
|
||||
|
||||
export const HeaderLarge = styled.h1`
|
||||
font-size: ${fontSizes.xxlarge};
|
||||
margin-bottom: ${px(units.plus)};
|
||||
|
||||
&:after {
|
||||
content: '.';
|
||||
visibility: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
export const HeaderMedium = styled<{ css: string }, 'h2'>('h2')`
|
||||
margin: ${px(units.plus)} 0;
|
||||
font-size: ${fontSizes.xlarge};
|
||||
${props => props.css};
|
||||
`;
|
||||
|
||||
export const HeaderSmall = styled<{ css: string }, 'h3'>('h3')`
|
||||
margin: ${px(units.plus)} 0;
|
||||
font-size: ${fontSizes.large};
|
||||
${props => props.css};
|
||||
`;
|
||||
|
||||
export const Tab = styled<{ selected: boolean }, 'div'>('div')`
|
||||
display: inline-block;
|
||||
font-size: ${fontSizes.large};
|
||||
padding: ${px(unit)} ${px(unit + units.quarter)};
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
border-bottom: ${props =>
|
||||
props.selected && `${units.quarter / 2}px solid ${colors.blue1}`};
|
||||
`;
|
|
@ -5,16 +5,25 @@
|
|||
*/
|
||||
|
||||
import { ESFilter } from 'elasticsearch';
|
||||
import { get } from 'lodash';
|
||||
import { oc } from 'ts-optchain';
|
||||
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
|
||||
import { ERROR_GROUP_ID, SERVICE_NAME } from '../../../common/constants';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
|
||||
import {
|
||||
ERROR_GROUP_ID,
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_SAMPLED
|
||||
} from '../../../common/constants';
|
||||
import { Setup } from '../helpers/setup_request';
|
||||
import { getTransaction } from '../transactions/get_transaction';
|
||||
|
||||
export interface ErrorGroupAPIResponse {
|
||||
transaction?: Transaction;
|
||||
error?: APMError;
|
||||
occurrencesCount?: number;
|
||||
}
|
||||
|
||||
// TODO: rename from "getErrorGroup" to "getErrorGroupSample" (since a single error is returned, not an errorGroup)
|
||||
export async function getErrorGroup({
|
||||
serviceName,
|
||||
groupId,
|
||||
|
@ -49,21 +58,30 @@ export async function getErrorGroup({
|
|||
size: 1,
|
||||
query: {
|
||||
bool: {
|
||||
filter
|
||||
filter,
|
||||
should: [{ term: { [TRANSACTION_SAMPLED]: true } }]
|
||||
}
|
||||
},
|
||||
sort: [
|
||||
{
|
||||
'@timestamp': 'desc'
|
||||
}
|
||||
{ _score: 'desc' }, // sort by _score first to ensure that errors with transaction.sampled:true ends up on top
|
||||
{ '@timestamp': { order: 'desc' } } // sort by timestamp to get the most recent error
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const resp = await client<APMError>('search', params);
|
||||
const error = oc(resp).hits.hits[0]._source();
|
||||
const transactionId = oc(error).transaction.id();
|
||||
const traceId: string | undefined = get(error, 'trace.id'); // cannot use oc because 'trace' doesn't exist on v1 errors
|
||||
|
||||
let transaction;
|
||||
if (transactionId) {
|
||||
transaction = await getTransaction(transactionId, traceId, setup);
|
||||
}
|
||||
|
||||
return {
|
||||
error: oc(resp).hits.hits[0]._source(),
|
||||
transaction,
|
||||
error,
|
||||
occurrencesCount: oc(resp).hits.total()
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from '../../../../common/constants';
|
||||
import { Setup } from '../../helpers/setup_request';
|
||||
|
||||
export type TransactionAPIResponse = Transaction | null;
|
||||
export type TransactionAPIResponse = Transaction | undefined;
|
||||
|
||||
export async function getTransaction(
|
||||
transactionId: string,
|
||||
|
@ -56,5 +56,5 @@ export async function getTransaction(
|
|||
}
|
||||
|
||||
const resp = await client<Transaction>('search', params);
|
||||
return oc(resp).hits.hits[0]._source() || null;
|
||||
return oc(resp).hits.hits[0]._source();
|
||||
}
|
||||
|
|
|
@ -76,6 +76,7 @@ interface ErrorV2 extends APMDocV2 {
|
|||
context: Context;
|
||||
transaction: {
|
||||
id: string; // transaction ID is required in v2
|
||||
sampled?: boolean;
|
||||
};
|
||||
error: {
|
||||
id: string; // ID is required in v2
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue